// Copyright 2016 CERN and GSI // // This program 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. // // This program 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 this program. If not, see <http://www.gnu.org/licenses/>. #include <silecs-communication/interface/equipment/SilecsPLC.h> #include <silecs-communication/interface/core/Diagnostic.h> #include <silecs-communication/interface/core/SilecsService.h> #include <silecs-communication/interface/core/WrapperAction.h> #include <silecs-communication/interface/equipment/SilecsCluster.h> #include <silecs-communication/interface/equipment/SilecsDevice.h> #include <silecs-communication/interface/equipment/SilecsBlock.h> #include <silecs-communication/interface/equipment/SilecsRegister.h> #include <silecs-communication/interface/equipment/PLCBlock.h> #include <silecs-communication/interface/equipment/PLCRegister.h> #include <silecs-communication/interface/utility/SilecsException.h> #include <silecs-communication/interface/utility/SilecsLog.h> #include <silecs-communication/interface/utility/StringUtilities.h> #include <silecs-communication/interface/utility/XMLParser.h> //Supported protocol #include <silecs-communication/interface/communication/MBConnection.h> #include <silecs-communication/interface/communication/SNAP7Connection.h> #include <silecs-communication/interface/communication/CNVConnection.h> #include <silecs-communication/interface/equipment/CNVBlock.h> #include <fstream> namespace Silecs { connMapType PLC::connMap_; PLC::PLC(Cluster* theCluster, std::string plcName, std::string plcIPaddr, string parameterFile) : theCluster_ (theCluster), IPaddr_(plcIPaddr), parameterFile_(parameterFile) { //Force PLC hostname lower-case name_ = plcName; StringUtilities::toLower(name_); LOG(ALLOC) << "PLC (create): " << name_ << "/ " << IPaddr_; // Upload the parameters file and build the PLC configuration extractDatabase(); //Do not synchronize retentive Registers by default (overwritten at connection time) synchroMode_ = NO_SYNCHRO; // Start the PLC Thread pThread_ = new Thread<WrapperAction>(); // create default contexts for all-devices transaction (normal and for synchronization) allDevicesTransaction_ = new Context(NULL/*no Device: for all devices)*/, false); allDevicesSynchronization_ = new Context(NULL/*no Device: for all devices)*/, true); // Instantiate the communication interface plcConn_ = NULL; isSharedConn_ = false; // No shared connection by default and for CNV protocol (PXI platform). } PLC::~PLC() { LOG(ALLOC) << "PLC (delete): " << name_; //Before removing the PLC resources, we need to interrupt all the related accesses (if connection is not shared) if (isSharedConn_ == false) disconnect(); // Remove PLC communication resources if (pThread_ != NULL) { delete pThread_; pThread_ = NULL; } if ((plcConn_ != NULL) && (isSharedConn_ == false)) { delete plcConn_; plcConn_ = NULL; } //theHeader object and the shared-connection objects (if any) must not be delete from the PLC since //it is shared between all the PLC instances. It will be removed by the Service destructor at the end. delete allDevicesTransaction_; delete allDevicesSynchronization_; // Remove the Device instances related to this PLC deviceVectorType::iterator pDeviceIter; for(pDeviceIter = deviceCol_.begin(); pDeviceIter != deviceCol_.end(); ++pDeviceIter) delete pDeviceIter->second; deviceCol_.clear(); // Remove the Block instances related to this PLC blockVectorType::iterator pBlockIter; for(pBlockIter = blockCol_.begin(); pBlockIter != blockCol_.end(); ++pBlockIter) { delete *pBlockIter; } } void PLC::connect(SynchroMode synchroMode, bool connectNow, bool compareChecksums) { //At first, Instantiate the PLC connection object if needed LOG((COMM|DIAG)) << "Attempt to establish connection ... "; /*plcConn_ =*/ getConnection(); //Check PLC is not already Enabled std::string connStr = name_ + "(" + IPaddr_ + ")" + ":" + theCluster_->getClassName() + "/v" + theCluster_->getClassVersion(); LOG(ERROR) << connStr; if ( (connectNow == false) && plcConn_->isEnabled() ) { //Trying to connect a connected PLC should not be necessary an ERROR //throw SilecsException(__FILE__, __LINE__, COMM_ALREADY_ENABLED, connStr); LOG(ERROR) << "This PLC connection is already enabled: " << connStr; return; } LOG(COMM) << (connectNow ? "Open" : "Enable") << " Connection request for " << connStr; //Store the synchonization mode for this particular PLC/Cluster connection request (can change at any connect() call. synchroMode_ = synchroMode; // Whatever the outcome of the connection that will follow, the PLC must be Enabled in any case. // Thus, even if the PLC is off and rejects the first connection to upload the Header (see updateHeader), // we will automatically attempt to reconnect later at the next PLC accesses (send/recv) ... again and again. plcConn_->enable(this, connectNow); if( compareChecksums ) { /* PLC memory could have change during a disconnection time (downloading or other). The Header registers must be reloaded for consistency checking. Do not continue the connection sequence if it is not even possible to load the header. */ LOG(COMM) << "getting checksum"; remoteChecksum_ = theHeader_->getRegister("_checksum")->getValULong(); if (remoteChecksum_ != localChecksum_) { //Client and PLC mapping are not consistent ... no transaction are possible! disconnect(); std::ostringstream message; message << "Checksum comparison failed. Checksum in parameter file: '" << localChecksum_ << "' Checksum in PLC: '" << remoteChecksum_<< "' Disconnecting PLC now."; throw SilecsException(__FILE__, __LINE__, CONFIG_CLIENT_PLC_NOT_CONSISTENT, message.str()); } } }; void PLC::disconnect() { LOG(COMM) << "Disconnection request for " << name_ << ":" << theCluster_->getClassName() << "/v" << theCluster_->getClassVersion(); //close the connection (also close the connection if shared with other cluster on the same PLC!) getConnection()->disable(this); synchroMode_ = NO_SYNCHRO; //Do not synchronize retentive Registers by default }; bool PLC::isEnabled() { return ((plcConn_ == NULL) ? false : plcConn_->isEnabled()); /*do not use getConnection() here*/} bool PLC::isConnected(bool connectNow) { return (connectNow ? getConnection()->doOpen(this): getConnection()->isConnected()); } unsigned int PLC::getThreadPriority() { LOG(SETUP) << "Thread RT-priority of PLC " << name_ << " is: " << pThread_->getPriority(); return pThread_->getPriority(); } void PLC::setThreadPriority(unsigned int rtprio) { LOG(SETUP) << "Set Thread RT-priority of PLC " << name_ << ": " << rtprio; return pThread_->setPriority(rtprio); } const std::string PLC::getParamsFileName() { return parameterFile_; } Device* PLC::getDevice(std::string deviceName) { deviceVectorType::iterator iter; for(iter = deviceCol_.begin(); iter != deviceCol_.end(); ++iter) { if (DEBUG & Log::topics_) LOG(COMM) << "getDevice - deviceName: '" << iter->first << "'"; } for(iter = deviceCol_.begin(); iter != deviceCol_.end(); ++iter) { if (iter->first == deviceName) { return iter->second; } } throw SilecsException(__FILE__, __LINE__, PARAM_UNKNOWN_DEVICE_NAME, deviceName+"/"+name_); return NULL;//to suppress warning } std::vector<std::string> PLC::getDeviceList() { std::vector<std::string> deviceList; deviceVectorType::iterator pDeviceIter; for(pDeviceIter = deviceCol_.begin(); pDeviceIter != deviceCol_.end(); ++pDeviceIter) { deviceList.push_back(pDeviceIter->first); } return deviceList; } std::string PLC::getName() { return name_; } std::string PLC::getIPAddress() { return IPaddr_; } std::string PLC::getDomain() { return domain_; } std::string PLC::getBrand() { return brand_; } std::string PLC::getSystem() { return system_; } std::string PLC::getModel() { return model_; } std::string PLC::getProtocolType() { return protocolType_; } std::string PLC::getProtocolMode() { return protocolMode_; } unsigned long PLC::getBaseAddress() { return baseAddr_; } deviceVectorType& PLC::getDeviceMap() { return deviceCol_; } std::string PLC::getLocalRelease() { return localRelease_; } std::string PLC::getLocalOwner() { return localOwner_; } std::string PLC::getLocalDate() { return localDate_; } unsigned long PLC::getLocalChecksum() { return localChecksum_; } void PLC::updateLocalData() { blockVectorType::iterator blockIter; for (blockIter = blockCol_.begin();blockIter!= blockCol_.end(); blockIter ++) { if ((*blockIter)->hasInputAccess()) { deviceVectorType::iterator pDeviceIter; for(pDeviceIter = deviceCol_.begin(); pDeviceIter != deviceCol_.end(); ++pDeviceIter) { if( pDeviceIter->second->hasBlock((*blockIter)->getName())) { LOG(COMM) << "Updating block '" << (*blockIter)->getName() << "' for device '" << pDeviceIter->second->getLabel()<< "'"; pDeviceIter->second->recv((*blockIter)->getName()); } } } } } int PLC::recvUnitCode(UnitCodeType& dataStruct) { return getConnection()->readUnitCode(this, dataStruct); } int PLC::recvUnitStatus(UnitStatusType& dataStruct) { return getConnection()->readUnitStatus(this, dataStruct); } int PLC::recvCPUInfo(CPUInfoType& dataStruct) { return getConnection()->readCPUInfo(this, dataStruct); } int PLC::recvCPInfo(CPInfoType& dataStruct) { return getConnection()->readCPInfo(this, dataStruct); } int PLC::recv(std::string blockName) { //Synchronous data receive //Execute the receive action for all the PLC devices (from the current thread) return getBlock(blockName, Input)->getTask()->execute(allDevicesTransaction_); } int PLC::send(std::string blockName) { //Synchronous data send //Execute the send action for all the PLC devices (from the current thread) return getBlock(blockName, Output)->getTask()->execute(allDevicesTransaction_); } void PLC::recvAsync(std::string blockName) { //Asynchronous data receive (used from Cluster level only) //Schedule task using asynchronous call for parallelism (submit to thread) pThread_->schedule(getBlock(blockName, Input)->getTask(), allDevicesTransaction_); } void PLC::sendAsync(std::string blockName) { //Asynchronous data send (used from Cluster level only) //Schedule task using asynchronous call for parallelism (submit to thread) pThread_->schedule(getBlock(blockName, Output)->getTask(), allDevicesTransaction_); } void PLC::waitAsync() { pThread_->waitForTaskCompletion(); } void PLC::extractDatabase() { LOG(SETUP) << "Extract SILECS configuration from: " << parameterFile_; XMLParser xmlParser(parameterFile_,true); // Extract general information =========================================== ElementXML rootNode = xmlParser.getFirstElementFromXPath("/SILECS-Param"); localRelease_ = rootNode.getAttribute("silecs-version"); ElementXML ownerNode = xmlParser.getFirstElementFromXPath("/SILECS-Param/Mapping-Info/Owner"); localOwner_ = ownerNode.getAttribute("user-login"); ElementXML generationNode = xmlParser.getFirstElementFromXPath("/SILECS-Param/Mapping-Info/Generation"); localDate_ = generationNode.getAttribute("date"); ElementXML deploymentNode = xmlParser.getFirstElementFromXPath("/SILECS-Param/Mapping-Info/Deployment"); StringUtilities::fromString(localChecksum_,deploymentNode.getAttribute("checksum") ); // Extract PLC general configuration ====================================== ElementXML mappingNode = xmlParser.getFirstElementFromXPath("/SILECS-Param/SILECS-Mapping"); //name_ = pEl->getStringAttribute("name"); already done before domain_ = mappingNode.getAttribute("domain"); brand_ = mappingNode.getAttribute("plc-brand"); system_ = mappingNode.getAttribute("plc-system"); protocolMode_ = mappingNode.getAttribute("protocol"); brandID_ = whichPLCBrand(brand_); systemID_ = whichPLCSystem(system_); protocolModeID_ = whichProtocolMode(protocolMode_); protocolTypeID_ = whichProtocolType(system_); switch(protocolTypeID_) { case MBProtocol: protocolType_ = "MODBUS-TCP"; break; case S7Protocol: protocolType_ = "SNAP7-TCP"; break; case CNVProtocol: protocolType_ = "CNV-TCP"; break; }; model_ = mappingNode.getAttribute("plc-model"); modelID_ = whichPLCModel(model_); StringUtilities::fromString(baseAddr_,mappingNode.getAttribute("address") ); usedMem_ = mappingNode.getAttribute("used-mem"); std::vector< boost::shared_ptr<ElementXML> > classNodes = mappingNode.childList_; std::vector< boost::shared_ptr<ElementXML> >::const_iterator classIter; for(classIter = classNodes.begin(); classIter != classNodes.end(); classIter++) { std::string className = (*classIter)->getAttribute("name"); boost::ptr_vector<ElementXML> blockNodes = xmlParser.getElementsFromXPath_throwIfEmpty("/SILECS-Param/SILECS-Mapping/SILECS-Class[@name='"+ className + "']/Block"); boost::ptr_vector<ElementXML>::const_iterator blockIter; boost::ptr_vector<ElementXML> instanceNodes = xmlParser.getElementsFromXPath_throwIfEmpty("/SILECS-Param/SILECS-Mapping/SILECS-Class[@name='"+ className + "']/Instance"); boost::ptr_vector<ElementXML>::const_iterator instanceIter; for(instanceIter = instanceNodes.begin(); instanceIter != instanceNodes.end(); instanceIter++) { std::string deviceName = (*instanceIter).getAttribute("label"); std::string deviceAddress = (*instanceIter).getAttribute("address"); Device* pDevice = new Device(this, deviceName, deviceAddress, blockNodes); deviceCol_.push_back(std::make_pair(deviceName, pDevice)); if(deviceName == "SilecsHeader") theHeader_ = pDevice; } for(blockIter = blockNodes.begin(); blockIter != blockNodes.end(); blockIter++) { std::string blockName = (*blockIter).getAttribute("name"); AccessType accessType = Block::whichAccessType((*blockIter).getAttribute("mode")); LOG((DIAG)) << "The block '" << blockName << " will be created with access-type: '" << accessType << "'."; Block* pBlock = 0; // Instantiate Input blocks ------------------------------------------------------ if (Service::withInputAccess(accessType)) //Input or InOut access { //Instantiate the block, forcing it to Input access for InOut case if(whichPLCBrand(brand_)==Ni) #ifdef NI_SUPPORT_ENABLED pBlock = new CNVInputBlock(this, (*pBlockElCol)[i], accessType); #else throw SilecsException(__FILE__, __LINE__, "Support for NI-Devices is disabled"); #endif else pBlock = new InputBlock(this,*blockIter, Input); blockCol_.push_back(pBlock); // FIXME: Currently a block is instatiated twice if it is READ+WRITE } // Instantiate Output blocks ------------------------------------------------------ if (Service::withOutputAccess(accessType)) //Output or InOut accessprotocol { //Instantiate the block, forcing it to Output access for InOut case if(whichPLCBrand(brand_)==Ni) #ifdef NI_SUPPORT_ENABLED pBlock = new CNVOutputBlock(this, (*pBlockElCol)[i], accessType); #else throw SilecsException(__FILE__, __LINE__, "Support for NI-Devices is disabled"); #endif else pBlock = new OutputBlock(this, *blockIter, Output); blockCol_.push_back(pBlock); // FIXME: Currently a block is instatiated twice if it is READ+WRITE } } //for blocks } //for classes } Connection* PLC::createConnection() { Connection* theConn = NULL; try { switch(protocolTypeID_) { case S7Protocol: theConn = new SNAP7Connection(this); break; case MBProtocol: theConn = new MBConnection(this); break; case CNVProtocol: #ifdef NI_SUPPORT_ENABLED theConn = new CNVConnection(this);break; #else throw SilecsException(__FILE__, __LINE__, "Support for NI-Devices is disabled"); #endif default: throw SilecsException(__FILE__, __LINE__, "Unknown protocolTypeID_"); } } catch (const SilecsException& ex) { //Something has failed. Nothing to do, we trust the re-open mechanism from now. LOG(ERROR) << ex.what(); throw ex; } if (DEBUG & Log::topics_) LOG(COMM) << "PLC::createConnection(): " << connID_ << ", isShared: " << (isSharedConn_? "true":"false"); return theConn; } Connection* PLC::getConnection() { if (plcConn_ == NULL) { LOG((COMM|DIAG)) << "No connection found. Attempt to establish connection ... "; //Connection not instantiated yet. Check if it will be shared or not? if (protocolTypeID_ != CNVProtocol) { /*TODO: get from the param file (extractDatabase): Major release??*/ isSharedConn_ = theCluster_->configuration.sharedConnection; } connID_ = (isSharedConn_) ? name_+"SharedConnection" : name_+theCluster_->getClassName(); if (isSharedConn_) { LOG((COMM|DIAG)) << "Creating shared connection"; //Create/re-used the shared connection connMapType::iterator iter = connMap_.find(connID_); if (iter == connMap_.end()) //one shared-connection per PLC { Connection* theConn = createConnection(); connMap_.insert(std::make_pair(connID_, theConn)); plcConn_ = theConn; } else { plcConn_ = iter->second; } } else { //Create the specific PLC/Cluster connection LOG((COMM|DIAG)) << "Creating non-shared connection"; plcConn_ = createConnection(); } } return plcConn_; } void PLC::deleteConnection() { // Remove all shared-connection if any (one per PLC) connMapType::iterator iter; for(iter = connMap_.begin(); iter != connMap_.end(); ++iter) { delete iter->second; } connMap_.clear(); } Block* PLC::getBlock(const std::string blockName, AccessType accessType) { blockVectorType::iterator block; for( block = blockCol_.begin(); block != blockCol_.end(); block++) { if( (*block)->getName() == blockName && (*block)->getAccessType() == accessType) return *block; } std::ostringstream error; error << "The block '" << blockName << "' was not found for the access type: '" << Block::whichAccessType(accessType) << "'"; throw SilecsException(__FILE__, __LINE__, error.str()); } blockVectorType& PLC::getBlockCol() { return blockCol_; } PLCBrand PLC::whichPLCBrand(std::string brand) { StringUtilities::toLower(brand); if (brand == "siemens") return Siemens; else if (brand == "schneider") return Schneider; else if (brand == "beckhoff") return Beckhoff; else if (brand == "rabbit") return Digi; else if (brand == "ni") return Ni; else throw SilecsException(__FILE__, __LINE__, DATA_UNKNOWN_PLC_MANUFACTURER, brand); } PLCModel PLC::whichPLCModel(std::string model) { StringUtilities::toLower(model); if (model == "simatic_et-200s") return ET200S; else if (model == "simatic_s7-300") return S7300; else if (model == "simatic_s7-400") return S7400; else if (model == "simatic_s7-1200") return S71200; else if (model == "simatic_s7-1500") return S71500; else if (model == "premium") return Premium; else if (model == "quantum") return Quantum; else if (model == "m340") return M340; else if (model == "bc9020") return BC9020; else if (model == "cx9020") return CX9020; else if (model == "rcm_4010") return RCM4010; else if (model == "rcm_2000") return RCM2000; else if (model == "compact_rio") return CompactRIO; else if (model == "pxi_rt") return PXIRT; else if (model == "pxi_windows") return PXIWindows; else if (model == "pc_windows") return PCWindows; else if (model == "other_support_cnv") return OtherSupportCNV; else throw SilecsException(__FILE__, __LINE__, DATA_UNKNOWN_PLC_MODEL, model); } PLCSystem PLC::whichPLCSystem(std::string system) { StringUtilities::toLower(system); if (system == "step-7") return Step7; if (system == "tia-portal") return TiaPortal; else if (system == "unity pro") return Unity; else if (system == "twincat") return TwinCat; else if (system == "standard-c") return StdC; else if (system == "labview") return Labview; else throw SilecsException(__FILE__, __LINE__, DATA_UNKNOWN_PLC_SYSTEM, system); } ProtocolType PLC::whichProtocolType(std::string system) { StringUtilities::toLower(system); if (system == "step-7") return S7Protocol; if (system == "tia-portal") return S7Protocol; else if (system == "unity pro") return MBProtocol; else if (system == "twincat") return MBProtocol; else if (system == "standard-c") return MBProtocol; else if (system == "labview") return CNVProtocol; else throw SilecsException(__FILE__, __LINE__, DATA_UNKNOWN_PLC_SYSTEM, system); } ProtocolMode PLC::whichProtocolMode(std::string mode) { StringUtilities::toLower(mode); if (mode == "block_mode") return BlockMode; else if (mode == "device_mode") return DeviceMode; else throw SilecsException(__FILE__, __LINE__, DATA_UNKNOWN_PROTOCOL_MODE, mode); } void PLC::updateStatus() { status_.connStatus_ = (getConnection()->isConnected()) ? Connected : Disconnected; status_.plcStatus_ = (getConnection()->isAlive()) ? On : Off; //#FLO problem: qui remet Undefined/Unknown pour ces 2 variables le cas echeant? if (DEBUG & Log::topics_) LOG(COMM) << "PLC Status:\n\t" << getName() << " mode: " << status_.getPLCStatusAsString() << "\n\t" << theCluster_->getHostName() << "/" << getName() << "/" << theCluster_->getClassName() << " connection: " << status_.getConnectionStatusAsString(); } void PLC::copyInToOut(const std::string blockName) { deviceVectorType::iterator pDeviceIter; for(pDeviceIter = deviceCol_.begin(); pDeviceIter != deviceCol_.end(); ++pDeviceIter) pDeviceIter->second->copyInToOut(blockName); } void PLC::setReadBlockAttributes(const std::string blockName, const unsigned long customAddress, const unsigned long customOffset, const unsigned long customSize) { //Set the custom offset/size of the referred Input block if any. //Will throw an exception if that block-name does not exist. getBlock(blockName, Input)->setCustomAttributes(customAddress, customOffset, customSize); } void PLC::resetReadBlockAttributes(const std::string blockName) { getBlock(blockName, Input)->resetCustomAttributes(); } void PLC::setWriteBlockAttributes(const std::string blockName, const unsigned long customAddress, const unsigned long customOffset, const unsigned long customSize) { //Set the custom address/offset/size of the referred Output block if any. //Will throw an exception if that block-name does not exist. getBlock(blockName, Output)->setCustomAttributes(customAddress, customOffset, customSize); } void PLC::resetWriteBlockAttributes(const std::string blockName) { getBlock(blockName, Output)->resetCustomAttributes(); } } // namespace