From 7acd515a9608dd46c5be62633966ca4ecb5150e2 Mon Sep 17 00:00:00 2001 From: Michael Zillgith Date: Wed, 18 May 2016 17:44:11 +0200 Subject: [PATCH] - WIP: server side logging --- src/iec61850/inc_private/logging.h | 18 +++ src/iec61850/inc_private/mms_mapping.h | 3 + src/iec61850/server/mms_mapping/logging.c | 111 ++++++++++++++++-- src/iec61850/server/mms_mapping/mms_mapping.c | 49 ++++---- src/logging/logging_api.h | 7 ++ src/mms/inc/mms_device_model.h | 3 + src/mms/iso_mms/server/mms_domain.c | 18 +++ src/mms/iso_mms/server/mms_journal_service.c | 108 +++++++++++++++++ tools/model_generator/genmodel.jar | Bin 83789 -> 84111 bytes .../com/libiec61850/scl/model/LogControl.java | 13 ++ .../tools/StaticModelGenerator.java | 27 +++-- 11 files changed, 316 insertions(+), 41 deletions(-) diff --git a/src/iec61850/inc_private/logging.h b/src/iec61850/inc_private/logging.h index 70b36f8b..afa3084b 100644 --- a/src/iec61850/inc_private/logging.h +++ b/src/iec61850/inc_private/logging.h @@ -30,6 +30,13 @@ typedef struct { LogicalNode* parentLN; LogStorage logStorage; + + uint64_t newEntryId; + uint64_t newEntryTime; + + uint64_t oldEntryId; + uint64_t oldEntryTime; + } LogInstance; typedef struct { @@ -46,6 +53,11 @@ typedef struct { MmsValue* mmsValue; MmsVariableSpecification* mmsType; + MmsValue* oldEntr; + MmsValue* oldEntrTm; + MmsValue* newEntr; + MmsValue* newEntrTm; + LogInstance* logInstance; bool enabled; @@ -58,6 +70,12 @@ typedef struct { LogInstance* LogInstance_create(LogicalNode* parentLN, const char* name); +void +LogInstance_setLogStorage(LogInstance* self, LogStorage logStorage); + +void +LogInstance_logSingleData(LogInstance* self, const char* dataRef, MmsValue* value, uint8_t flag); + void LogInstance_destroy(LogInstance* self); diff --git a/src/iec61850/inc_private/mms_mapping.h b/src/iec61850/inc_private/mms_mapping.h index 24a74484..0a1bb5a2 100644 --- a/src/iec61850/inc_private/mms_mapping.h +++ b/src/iec61850/inc_private/mms_mapping.h @@ -147,6 +147,9 @@ MmsMapping_setIedServer(MmsMapping* self, IedServer iedServer); void MmsMapping_setConnectionIndicationHandler(MmsMapping* self, IedConnectionIndicationHandler handler, void* parameter); +void +MmsMapping_setLogStorage(MmsMapping* self, const char* logRef, LogStorage logStorage); + void MmsMapping_installWriteAccessHandler(MmsMapping* self, DataAttribute* dataAttribute, WriteAccessHandler handler, void* parameter); diff --git a/src/iec61850/server/mms_mapping/logging.c b/src/iec61850/server/mms_mapping/logging.c index cdef07fe..905e0978 100644 --- a/src/iec61850/server/mms_mapping/logging.c +++ b/src/iec61850/server/mms_mapping/logging.c @@ -41,11 +41,15 @@ LogInstance_create(LogicalNode* parentLN, const char* name) { LogInstance* self = (LogInstance*) GLOBAL_MALLOC(sizeof(LogInstance)); - self->name = copyString(name); self->parentLN = parentLN; self->logStorage = NULL; + self->oldEntryId = 0; + self->oldEntryTime = 0; + self->newEntryId = 0; + self->newEntryTime = 0; + return self; } @@ -56,6 +60,49 @@ LogInstance_destroy(LogInstance* self) GLOBAL_FREEMEM(self); } +void +LogInstance_logSingleData(LogInstance* self, const char* dataRef, MmsValue* value, uint8_t flag) +{ + LogStorage logStorage = self->logStorage; + + if (logStorage != NULL) { + + printf("Log value - dataRef: %s flag: %i\n", dataRef, flag); + + uint64_t timestamp = Hal_getTimeInMs(); + + uint64_t entryID = LogStorage_addEntry(logStorage, timestamp); + + int dataSize = MmsValue_encodeMmsData(value, NULL, 0, false); + + uint8_t* data = GLOBAL_MALLOC(dataSize); + + MmsValue_encodeMmsData(value, data, 0, true); + + LogStorage_addEntryData(logStorage, entryID, dataRef, data, dataSize, flag); + + self->newEntryId = entryID; + self->newEntryTime = timestamp; + + } + else + printf("no log storage available!\n"); +} + +void +LogInstance_setLogStorage(LogInstance* self, LogStorage logStorage) +{ + self->logStorage = logStorage; + + LogStorage_getOldestAndNewestEntries(logStorage, &(self->newEntryId), &(self->newEntryTime), + &(self->oldEntryId), &(self->oldEntryTime)); + + printf("Attached storage to log: %s\n", self->name); + printf(" oldEntryID: %llu oldEntryTm: %llu\n", self->oldEntryId, self->oldEntryTime); + printf(" newEntryID: %llu newEntryTm: %llu\n", self->newEntryId, self->newEntryTime); + +} + LogControl* LogControl_create(LogicalNode* parentLN, MmsMapping* mmsMapping) { @@ -131,6 +178,20 @@ lookupLogControl(MmsMapping* self, MmsDomain* domain, char* lnName, char* object return NULL; } +static void +updateLogStatusInLCB(LogControl* self) +{ + LogInstance* logInstance = self->logInstance; + + if (logInstance != NULL) { + + MmsValue_setBinaryTime(self->oldEntrTm, logInstance->oldEntryTime); + MmsValue_setBinaryTime(self->newEntrTm, logInstance->newEntryTime); + + MmsValue_setOctetString(self->oldEntr, &(logInstance->oldEntryId), 8); + MmsValue_setOctetString(self->newEntr, &(logInstance->newEntryId), 8); + } +} MmsValue* @@ -138,6 +199,8 @@ LIBIEC61850_LOG_SVC_readAccessControlBlock(MmsMapping* self, MmsDomain* domain, { MmsValue* value = NULL; + printf("READ ACCESS LOG CB\n"); + char variableId[130]; strncpy(variableId, variableIdOrig, 129); @@ -164,6 +227,9 @@ LIBIEC61850_LOG_SVC_readAccessControlBlock(MmsMapping* self, MmsDomain* domain, LogControl* logControl = lookupLogControl(self, domain, lnName, objectName); if (logControl != NULL) { + + updateLogStatusInLCB(logControl); + if (varName != NULL) { value = MmsValue_getSubElement(logControl->mmsValue, logControl->mmsType, varName); } @@ -210,7 +276,13 @@ createTrgOps(LogControlBlock* reportControlBlock) { static bool enableLogging(LogControl* logControl) { - printf("enableLogging\n"); + printf("Enable LCB %s...\n", logControl->name); + + if (logControl->dataSetRef == NULL) { + printf(" no data set specified!\n"); + return false; + } + DataSet* dataSet = IedModel_lookupDataSet(logControl->mmsMapping->model, logControl->dataSetRef); if (dataSet == NULL) { @@ -220,11 +292,13 @@ enableLogging(LogControl* logControl) else logControl->dataSet = dataSet; + printf(" enabled\n"); + return true; } static MmsVariableSpecification* -createLogControlBlock(LogControlBlock* logControlBlock, +createLogControlBlock(MmsMapping* self, LogControlBlock* logControlBlock, LogControl* logControl) { MmsVariableSpecification* lcb = (MmsVariableSpecification*) GLOBAL_CALLOC(1, sizeof(MmsVariableSpecification)); @@ -263,7 +337,14 @@ createLogControlBlock(LogControlBlock* logControlBlock, lcb->typeSpec.structure.elements[1] = namedVariable; if (logControlBlock->logRef != NULL) { - mmsValue->value.structure.components[1] = MmsValue_newVisibleString(logControlBlock->logRef); + char logRef[130]; + + int maxLogRefLength = 129 - strlen(self->model->name); + + strcpy(logRef, self->model->name); + strncat(logRef, logControlBlock->logRef, maxLogRefLength); + + mmsValue->value.structure.components[1] = MmsValue_newVisibleString(logRef); } else { char* logRef = createString(4, logControl->domain->domainName, "/", logControlBlock->parent->name, @@ -303,6 +384,8 @@ createLogControlBlock(LogControlBlock* logControlBlock, mmsValue->value.structure.components[3] = MmsValue_newBinaryTime(false); + logControl->oldEntrTm = mmsValue->value.structure.components[3]; + /* NewEntrTm */ namedVariable = (MmsVariableSpecification*) GLOBAL_CALLOC(1, sizeof(MmsVariableSpecification)); namedVariable->name = copyString("NewEntrTm"); @@ -312,6 +395,8 @@ createLogControlBlock(LogControlBlock* logControlBlock, mmsValue->value.structure.components[4] = MmsValue_newBinaryTime(false); + logControl->newEntrTm = mmsValue->value.structure.components[4]; + /* OldEntr */ namedVariable = (MmsVariableSpecification*) GLOBAL_CALLOC(1, sizeof(MmsVariableSpecification)); namedVariable->name = copyString("OldEntr"); @@ -322,6 +407,8 @@ createLogControlBlock(LogControlBlock* logControlBlock, mmsValue->value.structure.components[5] = MmsValue_newOctetString(8, 8); + logControl->oldEntr = mmsValue->value.structure.components[5]; + /* NewEntr */ namedVariable = (MmsVariableSpecification*) GLOBAL_CALLOC(1, sizeof(MmsVariableSpecification)); namedVariable->name = copyString("NewEntr"); @@ -332,6 +419,8 @@ createLogControlBlock(LogControlBlock* logControlBlock, mmsValue->value.structure.components[6] = MmsValue_newOctetString(8, 8); + logControl->newEntr = mmsValue->value.structure.components[6]; + /* TrgOps */ namedVariable = (MmsVariableSpecification*) GLOBAL_CALLOC(1, sizeof(MmsVariableSpecification)); namedVariable->name = copyString("TrgOps"); @@ -426,9 +515,13 @@ getLogInstanceByLogRef(MmsMapping* self, const char* logRef) return NULL; } +#if 0 static LogInstance* getLogInstance(MmsMapping* self, LogicalNode* logicalNode, const char* logName) { + if (logName == NULL) + return NULL; + LinkedList logInstance = LinkedList_getNext(self->logInstances); while (logInstance != NULL) { @@ -444,6 +537,7 @@ getLogInstance(MmsMapping* self, LogicalNode* logicalNode, const char* logName) return NULL; } +#endif MmsVariableSpecification* Logging_createLCBs(MmsMapping* self, MmsDomain* domain, LogicalNode* logicalNode, @@ -470,10 +564,13 @@ Logging_createLCBs(MmsMapping* self, MmsDomain* domain, LogicalNode* logicalNode logControl->domain = domain; namedVariable->typeSpec.structure.elements[currentLcb] = - createLogControlBlock(logControlBlock, logControl); + createLogControlBlock(self, logControlBlock, logControl); //getLogInstanceByLogRef(self, logControlBlock->logRef); - logControl->logInstance = getLogInstance(self, logicalNode, logControlBlock->logRef); + //logControl->logInstance = getLogInstance(self, logicalNode, logControlBlock->logRef); + + if (logControlBlock->logRef != NULL) + logControl->logInstance = getLogInstanceByLogRef(self, logControlBlock->logRef); LinkedList_add(self->logControls, logControl); @@ -489,7 +586,7 @@ MmsMapping_setLogStorage(MmsMapping* self, const char* logRef, LogStorage logSto LogInstance* logInstance = getLogInstanceByLogRef(self, logRef); if (logInstance != NULL) - logInstance->logStorage = logStorage; + LogInstance_setLogStorage(logInstance, logStorage); //if (DEBUG_IED_SERVER) if (logInstance == NULL) diff --git a/src/iec61850/server/mms_mapping/mms_mapping.c b/src/iec61850/server/mms_mapping/mms_mapping.c index ae7d351e..32c159c3 100644 --- a/src/iec61850/server/mms_mapping/mms_mapping.c +++ b/src/iec61850/server/mms_mapping/mms_mapping.c @@ -1116,13 +1116,25 @@ createMmsDomainFromIedDevice(MmsMapping* self, LogicalDevice* logicalDevice) while (log != NULL) { - MmsDomain_addJournal(domain, log->name); + char journalName[65]; - printf("log->name: %s\n", log->name); + int nameLength = strlen(log->parent->name) + strlen(log->name); - LogInstance* logInstance = LogInstance_create(log->parent, log->name); + if (nameLength > 63) { + if (DEBUG_IED_SERVER) + printf("IED_SERVER: Log name %s invalid! Resulting journal name too long! Skip log\n", log->name); + } + else { + strcpy(journalName, log->parent->name); + strcat(journalName, "$"); + strcat(journalName, log->name); - LinkedList_add(self->logInstances, (void*) logInstance); + MmsDomain_addJournal(domain, journalName); + + LogInstance* logInstance = LogInstance_create(log->parent, log->name); + + LinkedList_add(self->logInstances, (void*) logInstance); + } log = log->sibling; } @@ -2586,7 +2598,7 @@ DataSet_isMemberValue(DataSet* dataSet, MmsValue* value, int* index) #if (CONFIG_IEC61850_LOG_SERVICE == 1) static bool -DataSet_isMemberValueWithRef(DataSet* dataSet, MmsValue* value, char* dataRef) +DataSet_isMemberValueWithRef(DataSet* dataSet, MmsValue* value, char* dataRef, const char* iedName) { int i = 0; @@ -2599,7 +2611,7 @@ DataSet_isMemberValueWithRef(DataSet* dataSet, MmsValue* value, char* dataRef) if (dataSetValue != NULL) { /* prevent invalid data set members */ if (isMemberValueRecursive(dataSetValue, value)) { if (dataRef != NULL) - sprintf(dataRef, "%s/%s", dataSetEntry->logicalDeviceName, dataSetEntry->variableName); + sprintf(dataRef, "%s%s/%s", iedName, dataSetEntry->logicalDeviceName, dataSetEntry->variableName); return true; } @@ -2695,30 +2707,13 @@ MmsMapping_triggerLogging(MmsMapping* self, MmsValue* value, LogInclusionFlag fl char dataRef[130]; - if (DataSet_isMemberValueWithRef(lc->dataSet, value, dataRef)) { - printf("Log value - dataRef: %s flag: %i\n", dataRef, flag); - - //TODO log to logInstance + if (DataSet_isMemberValueWithRef(lc->dataSet, value, dataRef, self->model->name)) { if (lc->logInstance != NULL) { - - LogStorage logStorage = lc->logInstance->logStorage; - - if (logStorage != NULL) { - - uint64_t entryID = LogStorage_addEntry(logStorage, Hal_getTimeInMs()); - - int dataSize = MmsValue_encodeMmsData(value, NULL, 0, false); - - uint8_t* data = GLOBAL_MALLOC(dataSize); - - MmsValue_encodeMmsData(value, data, 0, true); - - LogStorage_addEntryData(logStorage, entryID, dataRef, data, dataSize, flag); - - - } + LogInstance_logSingleData(lc->logInstance, dataRef, value, flag); } + else + printf("No log instance available!\n"); } diff --git a/src/logging/logging_api.h b/src/logging/logging_api.h index 575e5900..0f3b36fd 100644 --- a/src/logging/logging_api.h +++ b/src/logging/logging_api.h @@ -58,6 +58,9 @@ struct sLogStorage { bool (*getEntriesAfter) (LogStorage self, uint64_t startingTime, uint64_t entryID); + bool (*getOldestAndNewestEntries) (LogStorage self, uint64_t* newEntry, uint64_t* newEntryTime, + uint64_t* oldEntry, uint64_t* oldEntryTime); + void (*destroy) (LogStorage self); }; @@ -78,6 +81,10 @@ LogStorage_getEntriesAfter(LogStorage self, uint64_t startingTime, uint64_t entr void LogStorage_setCallbacks(LogStorage self, LogEntryCallback entryCallback, LogEntryDataCallback entryDataCallback, void* callbackParameter); +bool +LogStorage_getOldestAndNewestEntries(LogStorage self, uint64_t* newEntry, uint64_t* newEntryTime, + uint64_t* oldEntry, uint64_t* oldEntryTime); + void LogStorage_destroy(LogStorage self); diff --git a/src/mms/inc/mms_device_model.h b/src/mms/inc/mms_device_model.h index b2290ab4..404b8c8a 100644 --- a/src/mms/inc/mms_device_model.h +++ b/src/mms/inc/mms_device_model.h @@ -91,6 +91,9 @@ MmsDomain_getName(MmsDomain* self); void MmsDomain_addJournal(MmsDomain* self, const char* name); +MmsJournal +MmsDomain_getJournal(MmsDomain* self, const char* name); + /** * Delete a MmsDomain instance * diff --git a/src/mms/iso_mms/server/mms_domain.c b/src/mms/iso_mms/server/mms_domain.c index 6f406422..f8db8aed 100644 --- a/src/mms/iso_mms/server/mms_domain.c +++ b/src/mms/iso_mms/server/mms_domain.c @@ -84,6 +84,24 @@ MmsDomain_addJournal(MmsDomain* self, const char* name) LinkedList_add(self->journals, (void*) journal); } +MmsJournal +MmsDomain_getJournal(MmsDomain* self, const char* name) +{ + LinkedList journal = LinkedList_getNext(self->journals); + + while (journal != NULL) { + + MmsJournal mmsJournal = (MmsJournal) LinkedList_getData(journal); + + if (strcmp(mmsJournal->name, name) == 0) + return mmsJournal; + + journal = LinkedList_getNext(journal); + } + + return NULL; +} + bool MmsDomain_addNamedVariableList(MmsDomain* self, MmsNamedVariableList variableList) { diff --git a/src/mms/iso_mms/server/mms_journal_service.c b/src/mms/iso_mms/server/mms_journal_service.c index 8b8843c2..e0a2a4e5 100644 --- a/src/mms/iso_mms/server/mms_journal_service.c +++ b/src/mms/iso_mms/server/mms_journal_service.c @@ -27,6 +27,90 @@ #if (MMS_JOURNAL_SERVICE == 1) + +typedef struct sJournalVariable* JournalVariable; + +struct sJournalVariable { + char* tag; /* UTF8(1..255) */ + int tagSize; + + uint8_t* data; + int dataSize; + + JournalVariable next; +}; + +typedef struct { + uint8_t* entryID; + int entryIDSize; + + uint64_t timestamp; + + JournalVariable listOfVariables; +} JournalEntry; + + +struct sJournalEncoder { + uint8_t* buffer; + int maxSize; + int bufPos; + int currentEntryBufPos; /* store start buffer position of current entry in case the whole JournalEntry will become too long */ + +}; + +static bool +entryCallback(void* parameter, uint64_t timestamp, uint64_t entryID, bool moreFollow) +{ + if (moreFollow) + printf("Found entry ID:%llu timestamp:%llu\n", entryID, timestamp); + + return true; +} + +static bool +entryDataCallback (void* parameter, const char* dataRef, uint8_t* data, int dataSize, uint8_t reasonCode, bool moreFollow) +{ + if (moreFollow) { + printf(" EntryData: ref: %s\n", dataRef); + + MmsValue* value = MmsValue_decodeMmsData(data, 0, dataSize); + + char buffer[256]; + + MmsValue_printToBuffer(value, buffer, 256); + + printf(" value: %s\n", buffer); + } + + return true; +} + + +bool +MmsJournal_queryJournalByRange(MmsJournal self, uint64_t startTime, uint64_t endTime, ByteBuffer* response) +{ + // forward request to implementation class + + //TODO get handle of LogStorage + + struct sJournalEncoder encoderData; + + LogStorage logStorage; + + LogStorage_setCallbacks(logStorage, entryCallback, entryDataCallback, &encoderData); + + LogStorage_getEntries(logStorage, startTime, endTime); + + /* actual encoding will happen in callback handler. When getEntries returns the data is + * already encoded in the buffer. + */ + + // encoder header in response buffer + + // move encoded JournalEntry data to continue the buffer header +} + + static bool parseStringWithMaxLength(char* filename, int maxLength, uint8_t* buffer, int* bufPos, int maxBufPos , uint32_t invokeId, ByteBuffer* response) { @@ -268,6 +352,30 @@ mmsServer_handleReadJournalRequest( } //TODO check if required fields are present + //TODO check valid field combinations + + /* lookup journal */ + MmsServer server = connection->server; + + MmsDevice* mmsDevice = MmsServer_getDevice(connection->server); + + MmsDomain* mmsDomain = MmsDevice_getDomain(mmsDevice, domainId); + + if (mmsDomain == NULL) { + printf("Domain %s not found\n", domainId); + mmsServer_writeMmsRejectPdu(&invokeId, MMS_ERROR_REJECT_REQUEST_INVALID_ARGUMENT, response); + return; + } + + MmsJournal mmsJournal = MmsDomain_getJournal(mmsDomain, logName); + + if (mmsJournal == NULL) { + printf("Journal %s not found\n", logName); + mmsServer_writeMmsRejectPdu(&invokeId, MMS_ERROR_REJECT_REQUEST_INVALID_ARGUMENT, response); + return; + } + + printf("Read journal %s ...\n", mmsJournal->name); } #endif /* (MMS_JOURNAL_SERVICE == 1) */ diff --git a/tools/model_generator/genmodel.jar b/tools/model_generator/genmodel.jar index 3c50d337f1d0edd6a3cbe4d6e60b660d53df8005..89a9b09a43118f6b7b68e33ad46650d67c4d7b44 100644 GIT binary patch delta 17624 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 JeVrj-{}0ouo^A0g+S`=0yF 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 T|58!@6gPYrEcTb}0P+6;1QK<{ diff --git a/tools/model_generator/src/com/libiec61850/scl/model/LogControl.java b/tools/model_generator/src/com/libiec61850/scl/model/LogControl.java index 82cd3576..9960ee53 100644 --- a/tools/model_generator/src/com/libiec61850/scl/model/LogControl.java +++ b/tools/model_generator/src/com/libiec61850/scl/model/LogControl.java @@ -34,6 +34,7 @@ public class LogControl { private String ldInst = null; private String prefix = ""; private String lnClass = "LLN0"; + private String lnInst = ""; private String logName; private boolean logEna = true; private boolean reasonCode = true; @@ -49,6 +50,10 @@ public class LogControl { desc = ParserUtils.parseAttribute(logControlNode, "desc"); dataSet = ParserUtils.parseAttribute(logControlNode, "datSet"); + if (dataSet != null) + if (dataSet.equals("")) + dataSet = null; + ldInst = ParserUtils.parseAttribute(logControlNode, "ldInst"); prefix = ParserUtils.parseAttribute(logControlNode, "prefix"); @@ -57,11 +62,19 @@ public class LogControl { if (lnClassString != null) lnClass = lnClassString; + String lnInstString = ParserUtils.parseAttribute(logControlNode, "lnInst"); + + if (lnInstString != null) + lnInst = lnInstString; + logName = ParserUtils.parseAttribute(logControlNode, "logName"); if (logName == null) throw new SclParserException(logControlNode, "LogControl is missing required attribute \"logName\""); + if (logName.equals("")) + logName = null; + String intgPdString = ParserUtils.parseAttribute(logControlNode, "intgPd"); if (intgPdString != null) diff --git a/tools/model_generator/src/com/libiec61850/tools/StaticModelGenerator.java b/tools/model_generator/src/com/libiec61850/tools/StaticModelGenerator.java index fca09171..bd40376a 100644 --- a/tools/model_generator/src/com/libiec61850/tools/StaticModelGenerator.java +++ b/tools/model_generator/src/com/libiec61850/tools/StaticModelGenerator.java @@ -321,7 +321,7 @@ public class StaticModelGenerator { cOut.println(" (ModelNode*) &" + firstChildName); cOut.println("};\n"); - printLogicalNodeDefinitions(ldName, logicalDevice.getLogicalNodes()); + printLogicalNodeDefinitions(ldName, logicalDevice, logicalDevice.getLogicalNodes()); } @@ -541,7 +541,7 @@ public class StaticModelGenerator { } } - private void printLogicalNodeDefinitions(String ldName, List logicalNodes) { + private void printLogicalNodeDefinitions(String ldName, LogicalDevice logicalDevice, List logicalNodes) { for (int i = 0; i < logicalNodes.size(); i++) { LogicalNode logicalNode = logicalNodes.get(i); @@ -568,7 +568,7 @@ public class StaticModelGenerator { printReportControlBlocks(lnName, logicalNode); - printLogControlBlocks(lnName, logicalNode); + printLogControlBlocks(ldName, lnName, logicalNode, logicalDevice); printLogs(lnName, logicalNode); @@ -1077,7 +1077,7 @@ public class StaticModelGenerator { } } - private void printLogControlBlocks(String lnPrefix, LogicalNode logicalNode) + private void printLogControlBlocks(String ldName, String lnPrefix, LogicalNode logicalNode, LogicalDevice logicalDevice) { List logControlBlocks = logicalNode.getLogControlBlocks(); @@ -1086,7 +1086,7 @@ public class StaticModelGenerator { int lcbNumber = 0; for (LogControl lcb : logControlBlocks) { - printLogControlBlock(lnPrefix, lcb, lcbNumber, lcbCount); + printLogControlBlock(logicalDevice, lnPrefix, lcb, lcbNumber, lcbCount); lcbNumber++; } } @@ -1157,7 +1157,7 @@ public class StaticModelGenerator { this.logs.append(logString); } - private void printLogControlBlock(String lnPrefix, LogControl lcb, int lcbNumber, int lcbCount) + private void printLogControlBlock(LogicalDevice logicalDevice, String lnPrefix, LogControl lcb, int lcbNumber, int lcbCount) { String lcbVariableName = lnPrefix + "_lcb" + lcbNumber; @@ -1172,8 +1172,21 @@ public class StaticModelGenerator { else lcbString += "NULL, "; + + String logRef; + + if (lcb.getLdInst() == null) + logRef = logicalDevice.getInst() + "/"; + else + logRef = lcb.getLdInst() + "/"; + + if (lcb.getLnClass().equals("LLN0")) + logRef += "LLN0$"; + else + logRef += lcb.getLnClass() + "$"; + if (lcb.getLogName() != null) - lcbString += "\"" + lcb.getLogName() + "\", "; + lcbString += "\"" + logRef + lcb.getLogName() + "\", "; else lcbString += "NULL, ";