|
@@ -0,0 +1,907 @@
|
|
|
|
+/*
|
|
|
|
+ * Copyright 2016-2022 Michael Zillgith
|
|
|
|
+ *
|
|
|
|
+ * This file is part of lib60870-C
|
|
|
|
+ *
|
|
|
|
+ * lib60870-C 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.
|
|
|
|
+ *
|
|
|
|
+ * lib60870-C 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 lib60870-C. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
+ *
|
|
|
|
+ * See COPYING file for the complete license text.
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+#include "cs101_file_service.h"
|
|
|
|
+#include "lib_memory.h"
|
|
|
|
+#include "iec60870_slave.h"
|
|
|
|
+#include "lib60870_internal.h"
|
|
|
|
+#include "hal_time.h"
|
|
|
|
+
|
|
|
|
+typedef enum
|
|
|
|
+{
|
|
|
|
+ UNSELECTED_IDLE,
|
|
|
|
+ WAITING_FOR_FILE_CALL,
|
|
|
|
+ WAITING_FOR_SECTION_CALL,
|
|
|
|
+ TRANSMIT_SECTION,
|
|
|
|
+ WAITING_FOR_SECTION_ACK,
|
|
|
|
+ WAITING_FOR_FILE_ACK,
|
|
|
|
+ SEND_ABORT,
|
|
|
|
+ TRANSFER_COMPLETED,
|
|
|
|
+ WAITING_FOR_SECTION_READY,
|
|
|
|
+ RECEIVE_SECTION,
|
|
|
|
+} FileServerState;
|
|
|
|
+
|
|
|
|
+struct sCS101_FileServer
|
|
|
|
+{
|
|
|
|
+ struct sCS101_SlavePlugin plugin;
|
|
|
|
+
|
|
|
|
+ CS101_IFileReceiver fileReceiver;
|
|
|
|
+
|
|
|
|
+ CS101_FileReadyHandler fileReadyHandler; /* handler that is called when the master wants to send a file */
|
|
|
|
+ void* fileReadyHandlerParameter;
|
|
|
|
+
|
|
|
|
+ CS101_FilesAvailable filesAvailable;
|
|
|
|
+
|
|
|
|
+ int ca;
|
|
|
|
+ int ioa;
|
|
|
|
+ uint8_t oa;
|
|
|
|
+ uint16_t nof;
|
|
|
|
+
|
|
|
|
+ uint64_t lastSendTime;
|
|
|
|
+
|
|
|
|
+ FileServerState state;
|
|
|
|
+
|
|
|
|
+ uint8_t currentSectionNumber;
|
|
|
|
+ int currentSectionOffset;
|
|
|
|
+ int currentSectionSize;
|
|
|
|
+ uint8_t sectionChecksum;
|
|
|
|
+ uint8_t fileChecksum;
|
|
|
|
+
|
|
|
|
+ int maxSegmentSize; /* max. size of segment payload data */
|
|
|
|
+
|
|
|
|
+ uint64_t timeout;
|
|
|
|
+
|
|
|
|
+ CS101_IFileProvider selectedFile;
|
|
|
|
+ IMasterConnection selectedConnection;
|
|
|
|
+};
|
|
|
|
+
|
|
|
|
+static void
|
|
|
|
+sendCallFile(CS101_FileServer self, IMasterConnection connection, int oa)
|
|
|
|
+{
|
|
|
|
+ sCS101_StaticASDU _asdu;
|
|
|
|
+ uint8_t ioBuf[64];
|
|
|
|
+
|
|
|
|
+ CS101_AppLayerParameters alParams = IMasterConnection_getApplicationLayerParameters(connection);
|
|
|
|
+
|
|
|
|
+ /* send call file */
|
|
|
|
+ CS101_ASDU newAsdu = CS101_ASDU_initializeStatic(&_asdu, alParams, false, CS101_COT_FILE_TRANSFER,
|
|
|
|
+ oa, self->ca, false, false);
|
|
|
|
+
|
|
|
|
+ CS101_ASDU_addInformationObject(newAsdu, (InformationObject)
|
|
|
|
+ FileCallOrSelect_create((FileCallOrSelect) &ioBuf, self->ioa, self->nof, 0, 2/*=request-file*/));
|
|
|
|
+
|
|
|
|
+ IMasterConnection_sendASDU(connection, newAsdu);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void
|
|
|
|
+sendCallSection(CS101_FileServer self, IMasterConnection connection, int oa)
|
|
|
|
+{
|
|
|
|
+ sCS101_StaticASDU _asdu;
|
|
|
|
+ uint8_t ioBuf[64];
|
|
|
|
+
|
|
|
|
+ CS101_AppLayerParameters alParams = IMasterConnection_getApplicationLayerParameters(connection);
|
|
|
|
+
|
|
|
|
+ /* send call file */
|
|
|
|
+ CS101_ASDU newAsdu = CS101_ASDU_initializeStatic(&_asdu, alParams, false, CS101_COT_FILE_TRANSFER,
|
|
|
|
+ oa, self->ca, false, false);
|
|
|
|
+
|
|
|
|
+ CS101_ASDU_addInformationObject(newAsdu, (InformationObject)
|
|
|
|
+ FileCallOrSelect_create((FileCallOrSelect) &ioBuf, self->ioa, self->nof, self->currentSectionNumber, 6/*=request-section*/));
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Send CALL SECTION number: %i\n", self->currentSectionNumber);
|
|
|
|
+
|
|
|
|
+ IMasterConnection_sendASDU(connection, newAsdu);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void
|
|
|
|
+sendFileAck(CS101_FileServer self, IMasterConnection connection, int oa, uint8_t nos, uint8_t afq)
|
|
|
|
+{
|
|
|
|
+ sCS101_StaticASDU _asdu;
|
|
|
|
+ uint8_t ioBuf[64];
|
|
|
|
+
|
|
|
|
+ CS101_AppLayerParameters alParams = IMasterConnection_getApplicationLayerParameters(connection);
|
|
|
|
+
|
|
|
|
+ /* send call file */
|
|
|
|
+ CS101_ASDU newAsdu = CS101_ASDU_initializeStatic(&_asdu, alParams, false, CS101_COT_FILE_TRANSFER,
|
|
|
|
+ oa, self->ca, false, false);
|
|
|
|
+
|
|
|
|
+ CS101_ASDU_addInformationObject(newAsdu, (InformationObject)
|
|
|
|
+ FileACK_create((FileACK) &ioBuf, self->ioa, self->nof, nos, afq));
|
|
|
|
+
|
|
|
|
+ IMasterConnection_sendASDU(connection, newAsdu);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void
|
|
|
|
+sendSectionReady(CS101_FileServer self, IMasterConnection connection, int oa)
|
|
|
|
+{
|
|
|
|
+ sCS101_StaticASDU _asdu;
|
|
|
|
+ uint8_t ioBuf[64];
|
|
|
|
+
|
|
|
|
+ CS101_AppLayerParameters alParams = IMasterConnection_getApplicationLayerParameters(connection);
|
|
|
|
+
|
|
|
|
+ /* send call file */
|
|
|
|
+ CS101_ASDU newAsdu = CS101_ASDU_initializeStatic(&_asdu, alParams, false, CS101_COT_FILE_TRANSFER,
|
|
|
|
+ oa, self->ca, false, false);
|
|
|
|
+
|
|
|
|
+ CS101_ASDU_addInformationObject(newAsdu, (InformationObject)
|
|
|
|
+ SectionReady_create((SectionReady) &ioBuf, self->ioa, self->nof, self->currentSectionNumber, self->currentSectionSize, false));
|
|
|
|
+
|
|
|
|
+ IMasterConnection_sendASDU(connection, newAsdu);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void
|
|
|
|
+sendLastSection(CS101_FileServer self, IMasterConnection connection, int oa)
|
|
|
|
+{
|
|
|
|
+ sCS101_StaticASDU _asdu;
|
|
|
|
+ uint8_t ioBuf[64];
|
|
|
|
+
|
|
|
|
+ CS101_AppLayerParameters alParams = IMasterConnection_getApplicationLayerParameters(connection);
|
|
|
|
+
|
|
|
|
+ /* send call file */
|
|
|
|
+ CS101_ASDU newAsdu = CS101_ASDU_initializeStatic(&_asdu, alParams, false, CS101_COT_FILE_TRANSFER,
|
|
|
|
+ oa, self->ca, false, false);
|
|
|
|
+
|
|
|
|
+ CS101_ASDU_addInformationObject(newAsdu, (InformationObject)
|
|
|
|
+ FileLastSegmentOrSection_create((FileLastSegmentOrSection) &ioBuf, self->ioa, self->nof, self->currentSectionNumber, 1 /* file-transfer-without-deact */, self->fileChecksum));
|
|
|
|
+
|
|
|
|
+ IMasterConnection_sendASDU(connection, newAsdu);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void
|
|
|
|
+sendFileReady(CS101_FileServer self, IMasterConnection connection, int oa, uint32_t lof, bool positive)
|
|
|
|
+{
|
|
|
|
+ sCS101_StaticASDU _asdu;
|
|
|
|
+ uint8_t ioBuf[64];
|
|
|
|
+
|
|
|
|
+ CS101_AppLayerParameters alParams = IMasterConnection_getApplicationLayerParameters(connection);
|
|
|
|
+
|
|
|
|
+ /* send call file */
|
|
|
|
+ CS101_ASDU newAsdu = CS101_ASDU_initializeStatic(&_asdu, alParams, false, CS101_COT_FILE_TRANSFER,
|
|
|
|
+ oa, self->ca, false, false);
|
|
|
|
+
|
|
|
|
+ CS101_ASDU_addInformationObject(newAsdu, (InformationObject)
|
|
|
|
+ FileReady_create((FileReady) &ioBuf, self->ioa, self->nof, lof, positive));
|
|
|
|
+
|
|
|
|
+ IMasterConnection_sendASDU(connection, newAsdu);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static uint8_t
|
|
|
|
+calculateChecksum(uint8_t* data, int size)
|
|
|
|
+{
|
|
|
|
+ uint8_t checksum = 0;
|
|
|
|
+
|
|
|
|
+ int i;
|
|
|
|
+
|
|
|
|
+ for (i = 0; i < size; i++) {
|
|
|
|
+ checksum += data[i];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return checksum;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static bool
|
|
|
|
+sendSegment(CS101_FileServer self, IMasterConnection connection, int oa)
|
|
|
|
+{
|
|
|
|
+ int currentSegmentSize = self->currentSectionSize - self->currentSectionOffset;
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("sendSegment(%i/%i)\n", self->currentSectionOffset, self->currentSectionSize);
|
|
|
|
+
|
|
|
|
+ if (currentSegmentSize > 0) {
|
|
|
|
+ if (currentSegmentSize > self->maxSegmentSize)
|
|
|
|
+ currentSegmentSize = self->maxSegmentSize;
|
|
|
|
+
|
|
|
|
+ sCS101_StaticASDU _asdu;
|
|
|
|
+ uint8_t ioBuf[64];
|
|
|
|
+ uint8_t segmentData[255];
|
|
|
|
+
|
|
|
|
+ CS101_AppLayerParameters alParams = IMasterConnection_getApplicationLayerParameters(connection);
|
|
|
|
+
|
|
|
|
+ /* send call file */
|
|
|
|
+ CS101_ASDU newAsdu = CS101_ASDU_initializeStatic(&_asdu, alParams, false, CS101_COT_FILE_TRANSFER,
|
|
|
|
+ oa, self->ca, false, false);
|
|
|
|
+
|
|
|
|
+ self->selectedFile->getSegmentData(self->selectedFile, self->currentSectionNumber - 1, self->currentSectionOffset, currentSegmentSize, segmentData);
|
|
|
|
+
|
|
|
|
+ CS101_ASDU_addInformationObject(newAsdu, (InformationObject)
|
|
|
|
+ FileSegment_create((FileSegment) &ioBuf, self->ioa, self->nof, self->currentSectionNumber, segmentData, currentSegmentSize));
|
|
|
|
+
|
|
|
|
+ IMasterConnection_sendASDU(connection, newAsdu);
|
|
|
|
+
|
|
|
|
+ self->currentSectionOffset += currentSegmentSize;
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ self->sectionChecksum += calculateChecksum(segmentData, currentSegmentSize);
|
|
|
|
+
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void
|
|
|
|
+sendLastSegment(CS101_FileServer self, IMasterConnection connection, int oa)
|
|
|
|
+{
|
|
|
|
+ sCS101_StaticASDU _asdu;
|
|
|
|
+ uint8_t ioBuf[64];
|
|
|
|
+
|
|
|
|
+ CS101_AppLayerParameters alParams = IMasterConnection_getApplicationLayerParameters(connection);
|
|
|
|
+
|
|
|
|
+ /* send call file */
|
|
|
|
+ CS101_ASDU newAsdu = CS101_ASDU_initializeStatic(&_asdu, alParams, false, CS101_COT_FILE_TRANSFER,
|
|
|
|
+ oa, self->ca, false, false);
|
|
|
|
+
|
|
|
|
+ CS101_ASDU_addInformationObject(newAsdu,
|
|
|
|
+ (InformationObject)
|
|
|
|
+ FileLastSegmentOrSection_create((FileLastSegmentOrSection) &ioBuf, self->ioa, self->nof, self->currentSectionNumber,
|
|
|
|
+ 3 /* section-transfer-without-deact */, self->sectionChecksum));
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Send LAST SEGMENT (NoS=%i, CHS=%i)\n", self->currentSectionNumber, self->sectionChecksum);
|
|
|
|
+
|
|
|
|
+ self->fileChecksum += self->sectionChecksum;
|
|
|
|
+ self->sectionChecksum = 0;
|
|
|
|
+
|
|
|
|
+ IMasterConnection_sendASDU(connection, newAsdu);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ *
|
|
|
|
+ * Call inside asdu handler
|
|
|
|
+ */
|
|
|
|
+static CS101_SlavePlugin_Result
|
|
|
|
+CS101_FileServer_handleAsdu(void* parameter, IMasterConnection connection, CS101_ASDU asdu)
|
|
|
|
+{
|
|
|
|
+ CS101_FileServer self = (CS101_FileServer) parameter;
|
|
|
|
+
|
|
|
|
+ CS101_SlavePlugin_Result result = CS101_PLUGIN_RESULT_NOT_HANDLED;
|
|
|
|
+
|
|
|
|
+ IEC60870_5_TypeID typeId = CS101_ASDU_getTypeID(asdu);
|
|
|
|
+
|
|
|
|
+ /* check if type if is in range of file services */
|
|
|
|
+ if ((typeId >= F_FR_NA_1) && (typeId <= F_SC_NB_1)) {
|
|
|
|
+
|
|
|
|
+ /* check for timeout */
|
|
|
|
+ if (self->state != UNSELECTED_IDLE) {
|
|
|
|
+ if (Hal_getTimeInMs() > self->lastSendTime + self->timeout) {
|
|
|
|
+ DEBUG_PRINT ("Abort file transfer due to timeout\n");
|
|
|
|
+
|
|
|
|
+ self->state = UNSELECTED_IDLE;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ int oa = CS101_ASDU_getOA(asdu);
|
|
|
|
+
|
|
|
|
+ uint8_t ioBuf[250];
|
|
|
|
+
|
|
|
|
+ switch (typeId) {
|
|
|
|
+
|
|
|
|
+ case F_FR_NA_1: /* File Ready */
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Received file ready F_FR_NA_1\n");
|
|
|
|
+
|
|
|
|
+ if (self->fileReadyHandler) {
|
|
|
|
+
|
|
|
|
+ FileReady fileReady = (FileReady) CS101_ASDU_getElementEx(asdu, (InformationObject) ioBuf, 0);
|
|
|
|
+
|
|
|
|
+ int ioa = InformationObject_getObjectAddress((InformationObject) fileReady);
|
|
|
|
+
|
|
|
|
+ self->fileReceiver = NULL;
|
|
|
|
+
|
|
|
|
+ int errCode = 0;
|
|
|
|
+
|
|
|
|
+ if (self->fileReadyHandler) {
|
|
|
|
+ self->fileReceiver = self->fileReadyHandler(self->fileReadyHandlerParameter, CS101_ASDU_getCA(asdu), ioa, FileReady_getNOF(fileReady), FileReady_getLengthOfFile(fileReady), &errCode);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (self->fileReceiver) {
|
|
|
|
+ self->ca = CS101_ASDU_getCA(asdu);
|
|
|
|
+ self->ioa = ioa;
|
|
|
|
+ self->oa = oa;
|
|
|
|
+ self->nof = FileReady_getNOF(fileReady);
|
|
|
|
+ self->fileChecksum = 0;
|
|
|
|
+
|
|
|
|
+ sendCallFile(self, connection, oa);
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ self->state = WAITING_FOR_SECTION_READY;
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ if (errCode == 1) {
|
|
|
|
+ CS101_ASDU_setNegative(asdu, true);
|
|
|
|
+ CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_CA);
|
|
|
|
+ IMasterConnection_sendASDU(connection, asdu);
|
|
|
|
+ }
|
|
|
|
+ else if (errCode == 2) {
|
|
|
|
+ CS101_ASDU_setNegative(asdu, true);
|
|
|
|
+ CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_IOA);
|
|
|
|
+ IMasterConnection_sendASDU(connection, asdu);
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ self->ca = CS101_ASDU_getCA(asdu);
|
|
|
|
+ self->ioa = ioa;
|
|
|
|
+ self->nof = FileReady_getNOF(fileReady);
|
|
|
|
+ sendFileReady(self, connection, oa, 0, false);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ CS101_ASDU_setNegative(asdu, true);
|
|
|
|
+ CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_IOA);
|
|
|
|
+ IMasterConnection_sendASDU(connection, asdu);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ break;
|
|
|
|
+
|
|
|
|
+ case F_SR_NA_1: /* Section Ready */
|
|
|
|
+
|
|
|
|
+ if (self->state == WAITING_FOR_SECTION_READY) {
|
|
|
|
+
|
|
|
|
+ SectionReady sectionReady = (SectionReady) CS101_ASDU_getElementEx(asdu, (InformationObject) ioBuf, 0);
|
|
|
|
+
|
|
|
|
+ self->currentSectionNumber = SectionReady_getNameOfSection(sectionReady);
|
|
|
|
+ self->currentSectionOffset = 0;
|
|
|
|
+ self->currentSectionSize = SectionReady_getLengthOfSection(sectionReady);
|
|
|
|
+
|
|
|
|
+ /* send call section */
|
|
|
|
+ sendCallSection(self, connection, oa);
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ self->state = RECEIVE_SECTION;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ break;
|
|
|
|
+
|
|
|
|
+ case F_SG_NA_1: /* Segment */
|
|
|
|
+
|
|
|
|
+ if (self->state == RECEIVE_SECTION) {
|
|
|
|
+
|
|
|
|
+ FileSegment segment = (FileSegment) CS101_ASDU_getElementEx(asdu, (InformationObject) ioBuf, 0);
|
|
|
|
+
|
|
|
|
+ uint8_t nos = FileSegment_getNameOfSection(segment);
|
|
|
|
+ uint8_t los = FileSegment_getLengthOfSegment(segment);
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Received F_SG_NA_1(segment) (NoS=%i, LoS=%i)\n", nos, los);
|
|
|
|
+
|
|
|
|
+ if (self->fileReceiver) {
|
|
|
|
+ self->fileReceiver->segmentReceived(self->fileReceiver, nos, self->currentSectionOffset, los, FileSegment_getSegmentData(segment));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ self->currentSectionOffset += los;
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ DEBUG_PRINT ("Unexpected F_SG_NA_1(file segment)\n");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ break;
|
|
|
|
+
|
|
|
|
+ case F_LS_NA_1: /* Last Segment/Section */
|
|
|
|
+ {
|
|
|
|
+ DEBUG_PRINT ("Received F_LS_NA_1 (last segment/section)\n");
|
|
|
|
+
|
|
|
|
+ FileLastSegmentOrSection lastSection = (FileLastSegmentOrSection) CS101_ASDU_getElementEx(asdu, (InformationObject) ioBuf, 0);
|
|
|
|
+
|
|
|
|
+ uint8_t lsq = FileLastSegmentOrSection_getLSQ(lastSection);
|
|
|
|
+
|
|
|
|
+ if (self->state == RECEIVE_SECTION) {
|
|
|
|
+
|
|
|
|
+ if (lsq == 3 /* SECTION_TRANSFER_WITHOUT_DEACT */) {
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Send segment ACK for NoS=%i\n", FileLastSegmentOrSection_getNameOfSection(lastSection));
|
|
|
|
+
|
|
|
|
+ sendFileAck(self, connection, oa, FileLastSegmentOrSection_getNameOfSection(lastSection), 3 /* POS_ACK_SECTION */);
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ self->state = WAITING_FOR_SECTION_READY;
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else if (lsq == 2 /* FILE_TRANSFER_WITH_DEACT */) {
|
|
|
|
+ /* master aborted transfer */
|
|
|
|
+
|
|
|
|
+ if (self->fileReceiver) {
|
|
|
|
+ self->fileReceiver->finished(self->fileReceiver, CS101_FILE_ERROR_ABORTED_BY_REMOTE);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ self->state = UNSELECTED_IDLE;
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ DEBUG_PRINT("Unexpected LSQ\n");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else if (self->state == WAITING_FOR_SECTION_READY) {
|
|
|
|
+
|
|
|
|
+ if (lsq == 1 /* FILE_TRANSFER_WITHOUT_DEACT */) {
|
|
|
|
+ DEBUG_PRINT("Send file ACK\n");
|
|
|
|
+
|
|
|
|
+ sendFileAck(self, connection, oa, FileLastSegmentOrSection_getNameOfSection(lastSection), 1 /* POS_ACK_FILE */);
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+
|
|
|
|
+ if (self->fileReceiver) {
|
|
|
|
+ self->fileReceiver->finished(self->fileReceiver, CS101_FILE_ERROR_SUCCESS);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ self->state = UNSELECTED_IDLE;
|
|
|
|
+ }
|
|
|
|
+ else if (lsq == 2 /* FILE_TRANSFER_WITH_DEACT */) {
|
|
|
|
+ /* master aborted transfer */
|
|
|
|
+
|
|
|
|
+ if (self->fileReceiver) {
|
|
|
|
+ self->fileReceiver->finished(self->fileReceiver, CS101_FILE_ERROR_ABORTED_BY_REMOTE);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ self->state = UNSELECTED_IDLE;
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ DEBUG_PRINT("Unexpected LSQ\n");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ break;
|
|
|
|
+
|
|
|
|
+ case F_AF_NA_1: /* 124 - ACK file, ACK section */
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Received file/section ACK F_AF_NA_1\n");
|
|
|
|
+
|
|
|
|
+ if (self->state != UNSELECTED_IDLE) {
|
|
|
|
+
|
|
|
|
+ FileACK ack = (FileACK) CS101_ASDU_getElementEx(asdu, (InformationObject) ioBuf, 0);
|
|
|
|
+
|
|
|
|
+ uint8_t afq = FileACK_getAFQ(ack);
|
|
|
|
+
|
|
|
|
+ if (afq == 1 /* POS_ACK_FILE */) {
|
|
|
|
+ DEBUG_PRINT("Received positive file ACK\n");
|
|
|
|
+
|
|
|
|
+ if (self->state == WAITING_FOR_FILE_ACK) {
|
|
|
|
+
|
|
|
|
+ if (self->selectedFile)
|
|
|
|
+ self->selectedFile->transferComplete(self->selectedFile, true);
|
|
|
|
+
|
|
|
|
+ /* TODO remove file from list of available files */
|
|
|
|
+
|
|
|
|
+ self->selectedFile = NULL;
|
|
|
|
+ self->selectedConnection = NULL;
|
|
|
|
+
|
|
|
|
+ self->state = UNSELECTED_IDLE;
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ DEBUG_PRINT("Unexpected file transfer state --> abort file transfer\n");
|
|
|
|
+
|
|
|
|
+ self->state = SEND_ABORT;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else if ((afq & 0x0f) == 2 /* NEG_ACK_FILE */) {
|
|
|
|
+ DEBUG_PRINT("Received negative file ACK - stop transfer\n");
|
|
|
|
+
|
|
|
|
+ if (self->state == WAITING_FOR_FILE_ACK) {
|
|
|
|
+
|
|
|
|
+ if (self->selectedFile)
|
|
|
|
+ self->selectedFile->transferComplete(self->selectedFile, false);
|
|
|
|
+
|
|
|
|
+ self->selectedFile = NULL;
|
|
|
|
+ self->selectedConnection = NULL;
|
|
|
|
+
|
|
|
|
+ self->state = UNSELECTED_IDLE;
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ DEBUG_PRINT("Unexpected file transfer state --> abort file transfer\n");
|
|
|
|
+
|
|
|
|
+ self->state = SEND_ABORT;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else if ((afq & 0x0f) == 4 /* NEG_ACK_SECTION */) {
|
|
|
|
+ DEBUG_PRINT("Received negative file section ACK - repeat section\n");
|
|
|
|
+
|
|
|
|
+ if (self->state == WAITING_FOR_SECTION_ACK) {
|
|
|
|
+ self->currentSectionOffset = 0;
|
|
|
|
+ self->sectionChecksum = 0;
|
|
|
|
+
|
|
|
|
+ sendSectionReady(self, connection, oa);
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ self->state = TRANSMIT_SECTION;
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ DEBUG_PRINT("Unexpected file transfer state --> abort file transfer\n");
|
|
|
|
+
|
|
|
|
+ self->state = SEND_ABORT;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else if ((afq & 0x0f) == 3 /* POS_ACK_SECTION */) {
|
|
|
|
+ DEBUG_PRINT("Received positive section ACK\n");
|
|
|
|
+
|
|
|
|
+ if (self->state == WAITING_FOR_SECTION_ACK) {
|
|
|
|
+ self->currentSectionNumber++;
|
|
|
|
+
|
|
|
|
+ int nextSectionSize = self->selectedFile->getSectionSize(self->selectedFile, self->currentSectionNumber - 1);
|
|
|
|
+
|
|
|
|
+ self->currentSectionOffset = 0;
|
|
|
|
+
|
|
|
|
+ if (nextSectionSize <= 0) {
|
|
|
|
+ DEBUG_PRINT("Received positive file section ACK - send last section indication\n");
|
|
|
|
+
|
|
|
|
+ sendLastSection(self, connection, oa);
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ self->state = WAITING_FOR_FILE_ACK;
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ DEBUG_PRINT("Received positive file section ACK - send next section ready indication");
|
|
|
|
+
|
|
|
|
+ self->currentSectionSize = nextSectionSize;
|
|
|
|
+
|
|
|
|
+ sendSectionReady(self, connection, oa);
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ self->state = WAITING_FOR_SECTION_CALL;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ self->sectionChecksum = 0;
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ DEBUG_PRINT("Unexpected file transfer state --> abort file transfer\n");
|
|
|
|
+
|
|
|
|
+ self->state = SEND_ABORT;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_CA);
|
|
|
|
+ IMasterConnection_sendASDU(connection, asdu);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ break;
|
|
|
|
+
|
|
|
|
+ case F_SC_NA_1: /* 122 - Call/Select directory/file/section */
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Received call/select F_SC_NA_1\n");
|
|
|
|
+
|
|
|
|
+ if (CS101_ASDU_getCOT(asdu) == CS101_COT_FILE_TRANSFER)
|
|
|
|
+ {
|
|
|
|
+ FileCallOrSelect sc = (FileCallOrSelect) CS101_ASDU_getElementEx(asdu, (InformationObject) ioBuf, 0);
|
|
|
|
+
|
|
|
|
+ uint8_t scq = FileCallOrSelect_getSCQ(sc);
|
|
|
|
+ int ioa = InformationObject_getObjectAddress((InformationObject) sc);
|
|
|
|
+ uint16_t nof = FileCallOrSelect_getNOF(sc);
|
|
|
|
+
|
|
|
|
+ if (scq == 1 /* SELECT_FILE */) {
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Received SELECT FILE\n");
|
|
|
|
+
|
|
|
|
+ if (self->state == UNSELECTED_IDLE) {
|
|
|
|
+
|
|
|
|
+ CS101_IFileProvider file = NULL;
|
|
|
|
+
|
|
|
|
+ int errCode = 0;
|
|
|
|
+
|
|
|
|
+ if (self->filesAvailable)
|
|
|
|
+ file = self->filesAvailable->getFile(self->filesAvailable->parameter, CS101_ASDU_getCA(asdu), ioa, nof, &errCode);
|
|
|
|
+
|
|
|
|
+ if (file == NULL) {
|
|
|
|
+
|
|
|
|
+ if (errCode == 1) {
|
|
|
|
+ CS101_ASDU_setNegative(asdu, true);
|
|
|
|
+ CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_CA);
|
|
|
|
+ IMasterConnection_sendASDU(connection, asdu);
|
|
|
|
+ }
|
|
|
|
+ else if (errCode == 2) {
|
|
|
|
+ CS101_ASDU_setNegative(asdu, true);
|
|
|
|
+ CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_IOA);
|
|
|
|
+ IMasterConnection_sendASDU(connection, asdu);
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ self->ca = CS101_ASDU_getCA(asdu);
|
|
|
|
+ self->ioa = ioa;
|
|
|
|
+ self->nof = nof;
|
|
|
|
+ sendFileReady(self, connection, oa, 0, false);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ /* TODO check if file is already selected by other client */
|
|
|
|
+
|
|
|
|
+ self->selectedFile = file;
|
|
|
|
+ self->selectedConnection = connection;
|
|
|
|
+ self->ioa = ioa;
|
|
|
|
+ self->ca = CS101_ASDU_getCA(asdu);
|
|
|
|
+ self->nof = nof;
|
|
|
|
+
|
|
|
|
+ int fileSize = file->getFileSize(file);
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Send FILE READY\n");
|
|
|
|
+
|
|
|
|
+ sendFileReady(self, connection, oa, fileSize, true);
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ self->state = WAITING_FOR_FILE_CALL;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ DEBUG_PRINT("Unexpected select file message\n");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else if (scq == 3 /* DEACTIVATE_FILE */) {
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Received DEACTIVATE FILE\n");
|
|
|
|
+
|
|
|
|
+ if (self->state == UNSELECTED_IDLE) {
|
|
|
|
+ self->selectedFile = NULL;
|
|
|
|
+ self->selectedConnection = NULL;
|
|
|
|
+
|
|
|
|
+ self->state = UNSELECTED_IDLE;
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ DEBUG_PRINT("Unexpected DEACTIVATE FILE message\n");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else if (scq == 2 /* REQUEST_FILE */) {
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Received CALL FILE\n");
|
|
|
|
+
|
|
|
|
+ if (self->state == WAITING_FOR_FILE_CALL) {
|
|
|
|
+
|
|
|
|
+ /* TODO check if NoF matches */
|
|
|
|
+
|
|
|
|
+ if ((ioa != self->ioa) || (CS101_ASDU_getCA(asdu) != self->ca)) {
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Call for file that is not selected!\n");
|
|
|
|
+
|
|
|
|
+ if (CS101_ASDU_getCA(asdu) != self->ca)
|
|
|
|
+ CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_CA);
|
|
|
|
+ else
|
|
|
|
+ CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_IOA);
|
|
|
|
+
|
|
|
|
+ CS101_ASDU_setNegative(asdu, true);
|
|
|
|
+
|
|
|
|
+ IMasterConnection_sendASDU(connection, asdu);
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ self->currentSectionNumber = 1;
|
|
|
|
+ self->currentSectionOffset = 0;
|
|
|
|
+ self->fileChecksum = 0;
|
|
|
|
+ self->currentSectionSize = self->selectedFile->getSectionSize(self->selectedFile, 0);
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Send SECTION READY\n");
|
|
|
|
+
|
|
|
|
+ sendSectionReady(self, connection, oa);
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ self->state = WAITING_FOR_SECTION_CALL;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ DEBUG_PRINT("Unexpected CALL FILE message\n");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else if (scq == 6 /* REQUEST_SECTION */ ) {
|
|
|
|
+
|
|
|
|
+ uint8_t nos = FileCallOrSelect_getNameOfSection(sc);
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Received CALL SECTION (NoS = %i)\n", nos);
|
|
|
|
+
|
|
|
|
+ if (self->state == WAITING_FOR_SECTION_CALL) {
|
|
|
|
+
|
|
|
|
+ /* TODO check if NoF matches */
|
|
|
|
+
|
|
|
|
+ if ((ioa != self->ioa) || (CS101_ASDU_getCA(asdu) != self->ca)) {
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Call for file that is not selected!\n");
|
|
|
|
+
|
|
|
|
+ if (CS101_ASDU_getCA(asdu) != self->ca)
|
|
|
|
+ CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_CA);
|
|
|
|
+ else
|
|
|
|
+ CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_IOA);
|
|
|
|
+
|
|
|
|
+ CS101_ASDU_setNegative(asdu, true);
|
|
|
|
+
|
|
|
|
+ IMasterConnection_sendASDU(connection, asdu);
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+
|
|
|
|
+ if (CS101_ASDU_isNegative(asdu)) {
|
|
|
|
+
|
|
|
|
+ self->currentSectionNumber++;
|
|
|
|
+ self->currentSectionOffset = 0;
|
|
|
|
+
|
|
|
|
+ self->currentSectionSize = self->selectedFile->getSectionSize(self->selectedFile, self->currentSectionNumber - 1);
|
|
|
|
+
|
|
|
|
+ if (self->currentSectionSize > 0) {
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Send F_SR_NA_1 (section ready) (NoS = %i)\n", self->currentSectionNumber);
|
|
|
|
+
|
|
|
|
+ sendSectionReady(self, connection, oa);
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ self->state = WAITING_FOR_SECTION_CALL;
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+
|
|
|
|
+ DEBUG_PRINT("Send F_LS_NA_1 (last section))\n");
|
|
|
|
+
|
|
|
|
+ sendLastSection(self, connection, oa);
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ self->state = WAITING_FOR_FILE_ACK;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else { /* positive */
|
|
|
|
+ self->currentSectionSize = self->selectedFile->getSectionSize(self->selectedFile, nos - 1);
|
|
|
|
+
|
|
|
|
+ if (self->currentSectionSize > 0) {
|
|
|
|
+ self->currentSectionNumber = nos;
|
|
|
|
+ self->currentSectionOffset = 0;
|
|
|
|
+
|
|
|
|
+ self->state = TRANSMIT_SECTION;
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ DEBUG_PRINT("Unexpected number of section -> send negative confirm\n");
|
|
|
|
+
|
|
|
|
+ CS101_ASDU_setNegative(asdu, true);
|
|
|
|
+
|
|
|
|
+ IMasterConnection_sendASDU(connection, asdu);
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ DEBUG_PRINT("Unexpected CALL SECTION message\n");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+ else if (CS101_ASDU_getCOT(asdu) == CS101_COT_REQUEST) {
|
|
|
|
+ /* call directory */
|
|
|
|
+
|
|
|
|
+ /* TODO send directory */
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ DEBUG_PRINT("Received unexpected COT = %i\n", CS101_ASDU_getCOT(asdu));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ break;
|
|
|
|
+
|
|
|
|
+ default:
|
|
|
|
+ DEBUG_PRINT("Received unexpected type ID %i in file service\n", typeId);
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ result = CS101_PLUGIN_RESULT_HANDLED;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ return result;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+static void
|
|
|
|
+CS101_FileServer_runTask(void* parameter, IMasterConnection connection)
|
|
|
|
+{
|
|
|
|
+ CS101_FileServer self = (CS101_FileServer) parameter;
|
|
|
|
+
|
|
|
|
+ if (self->state != UNSELECTED_IDLE) {
|
|
|
|
+
|
|
|
|
+ if (self->state == TRANSMIT_SECTION) {
|
|
|
|
+
|
|
|
|
+ if (self->selectedConnection == connection) {
|
|
|
|
+
|
|
|
|
+ if (self->selectedFile) {
|
|
|
|
+
|
|
|
|
+ if (sendSegment(self, connection, self->oa) == false) {
|
|
|
|
+ sendLastSegment(self, connection, self->oa);
|
|
|
|
+
|
|
|
|
+ self->fileChecksum += self->sectionChecksum;
|
|
|
|
+ self->sectionChecksum = 0;
|
|
|
|
+
|
|
|
|
+ self->lastSendTime = Hal_getTimeInMs();
|
|
|
|
+ self->state = WAITING_FOR_SECTION_ACK;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /* check for timeout */
|
|
|
|
+ if (Hal_getTimeInMs() > self->lastSendTime + self->timeout) {
|
|
|
|
+ DEBUG_PRINT ("Abort file transfer due to timeout\n");
|
|
|
|
+
|
|
|
|
+ self->state = UNSELECTED_IDLE;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+bool (*handleAsdu) (void* parameter, IMasterConnection connection, CS101_ASDU asdu);
|
|
|
|
+void (*runTask) (void* parameter, IMasterConnection connection);
|
|
|
|
+
|
|
|
|
+void* parameter;
|
|
|
|
+
|
|
|
|
+CS101_FileServer
|
|
|
|
+CS101_FileServer_create(CS101_AppLayerParameters alParams)
|
|
|
|
+{
|
|
|
|
+ CS101_FileServer self = (CS101_FileServer) GLOBAL_CALLOC(1, sizeof(struct sCS101_FileServer));
|
|
|
|
+
|
|
|
|
+ if (self) {
|
|
|
|
+ self->timeout = 3000;
|
|
|
|
+ self->state = UNSELECTED_IDLE;
|
|
|
|
+
|
|
|
|
+ self->maxSegmentSize = FileSegment_GetMaxDataSize(alParams);
|
|
|
|
+
|
|
|
|
+ self->plugin.parameter = self;
|
|
|
|
+ self->plugin.handleAsdu = CS101_FileServer_handleAsdu;
|
|
|
|
+ self->plugin.runTask = CS101_FileServer_runTask;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return self;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void
|
|
|
|
+CS101_FileServer_destroy(CS101_FileServer self)
|
|
|
|
+{
|
|
|
|
+ GLOBAL_FREEMEM(self);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void
|
|
|
|
+CS101_FileServer_setFilesAvailableIfc(CS101_FileServer self, CS101_FilesAvailable ifc)
|
|
|
|
+{
|
|
|
|
+ self->filesAvailable = ifc;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void
|
|
|
|
+CS101_FileServer_setFileReadyHandler(CS101_FileServer self, CS101_FileReadyHandler handler, void* parameter)
|
|
|
|
+{
|
|
|
|
+ self->fileReadyHandler = handler;
|
|
|
|
+ self->fileReadyHandlerParameter = parameter;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+CS101_SlavePlugin
|
|
|
|
+CS101_FileServer_getSlavePlugin(CS101_FileServer self)
|
|
|
|
+{
|
|
|
|
+ return &(self->plugin);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+
|