Skip to content
Snippets Groups Projects
SilecsConnection.cpp 17.2 KiB
Newer Older
// 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 <fcntl.h>
#include <signal.h>

#include <silecs-communication/interface/equipment/SilecsCluster.h>
#include <silecs-communication/interface/utility/SilecsLog.h>
#include <silecs-communication/interface/communication/SilecsConnection.h>
#include <silecs-communication/interface/equipment/SilecsPLC.h>
#include <silecs-communication/interface/communication/ietype.h>

namespace Silecs
{

	// static definition
	bool Connection::isAlive_ = false;
	const unsigned int Connection::numberConn_ = 3;

	//------------------------------------------------------------------------------------------------------------------------------------------------
	Connection::Connection(PLC* thePLC)
	{
		// Start the PLC Thread
		readMux_ 	= new Mutex("readMux");
		writeMux_ 	= new Mutex("writeMux");
		connMux_ 	= new Mutex("connMux");

		//Not yet allowed, not yet connected!
		isEnabled_    = false;
		isConnected_  = false;

		//Reset the Reconnection mechanism
		timeConn_ = NULL;
		delayConn_ = UrgentConnection;   //initial reconnection delay
		remainConn_ = numberConn_; 		 //initial number of attempt
	}


	//------------------------------------------------------------------------------------------------------------------------------------------------
	Connection::~Connection()
	{
	    //Connection has been closed before from the concrete connection object
		if (timeConn_ != NULL) delete timeConn_;

		delete connMux_;
		delete writeMux_;
		delete readMux_;
	}

	//------------------------------------------------------------------------------------------------------------------------------------------------
	bool Connection::enable(PLC* thePLC, bool connectNow)
	{
		{
			Lock lock(connMux_);
			isEnabled_ = true;
		}
		if (connectNow) doOpen(thePLC);
		return isConnected_;
	}

	//------------------------------------------------------------------------------------------------------------------------------------------------
	void Connection::disable(PLC* thePLC)
	{
        if (!isEnabled_) {
            LOG(DEBUG) << "Trying to disconnect a PLC that is not enabled: " << thePLC->getName();
            return;
        }

        Lock lock(connMux_);
        {
        	Lock lock(readMux_);
        	{
        		Lock lock(writeMux_);
        		isEnabled_ = false;
        		doClose(thePLC, /*withLock =*/false);
        	}
        }
	}

	//------------------------------------------------------------------------------------------------------------------------------------------------
	bool Connection::doOpen(PLC* thePLC)
	{
		//LOG((COMM|DIAG)) << "Start attempt to open connection ..."; .. commented, seem like this creates to many log-entries for KIBANA
        bool isReachable    = false;
        bool isOpen         = false;
        bool justConnected  = false;

		{
			Lock lock(connMux_);
			if (isConnected_)
			{	if (!isEnabled_)
				{
					LOG((COMM|DIAG)) << "The PLC is connected but the client wants to disable the transactions";
					doClose(thePLC, /*withLock =*/false);
				}
				return isConnected_;
			}

			if (!isEnabled_)
			{
				LOG((COMM|DIAG)) << "The communication currently is not enabled";
				return isConnected_;
			}

			if (isTimeToReconnect())
			{
                if (isReachable = (ping((char *) thePLC->getName().c_str(), NULL) == 0))
                {
                    LOG((COMM|DIAG)) << "It's time to reconnect";

                    // It's time to open the connection according to the (re)connection timing
                    // Let's try several times with limited delay (some ms).
                    // It allows wake-up frozen PLC (SIEMENS in particular) after long stop period.
                    bool isOpen  = false;
                    unsigned int nbConn = 2;    //for fast low-level iteration
                    for(unsigned int i = 0; i<nbConn; i++)
                    {
                        LOG((COMM|DIAG)) << "Attempt to open PLC connection ....";
                        isOpen = open(thePLC);
                        if(isOpen)
                        {
                            LOG((COMM|DIAG)) << "Connection opened successfully";
                            break;
                        }
                        usleep(100000); // wait 100ms
                    }
                }//pingable?

                if(!isOpen)
                {
                    logError(thePLC,isReachable);
                    return isConnected_;
                }

                if (thePLC->isSharedConnection())
                {
                    std::ostringstream os;
                    os << "Shared connection with " << thePLC->getName() << " is established.";
                    if (thePLC->theHeader_ != NULL) TRACE("info") << os.str();
                        LOG(COMM) << os.str();
                    std::ostringstream os;
                    os << "Connection with " << thePLC->getName() <<
                            ":" << thePLC->theCluster_->getClassName() <<
                            "/v" << thePLC->theCluster_->getClassVersion() <<
                            " is established.";
                    if (thePLC->theHeader_ != NULL) TRACE("info") << os.str();
                        LOG(COMM) << os.str();
                }

                isAlive_ = true;
                isConnected_ = true;
                justConnected = true; //retentive registers synchronization is required!

                //Connection status has changed: update the diagnostic variables
                LOG((COMM|DIAG)) << "Updating PLC status";
                updateStatus(thePLC);

			}
		}//release lock

		/* Process the Retentive registers synchronization each time the PLC is just (re)connected.
		 * This is a recursive call: performs task::execute method from doOpen that is
		 * called into execute itself. The recursion is terminated when SilecsHeader connection is closed finally.
		 */
		if (justConnected)
		{
			LOG((COMM|DIAG)) << "First connection - performing registers synchronization";
			thePLC->updateLocalData();
		}
		LOG((COMM|DIAG)) << "isConnected_:" << isConnected_;
		return isConnected_;
	}


	//------------------------------------------------------------------------------------------------------------------------------------------------
	void Connection::doClose(PLC* thePLC, bool withLock)
	{
		//This process that can be called inside and outside protected section.
		//withLock argument is used to avoid Recursive mutex that is not supported
		//by LynxOS platform.

		if(withLock)
			Lock lock(connMux_);

		if (isConnected_)
		{
			if (close(thePLC))
			{
			  isConnected_ = false;

			  //Connection status has changed: update the diagnostic variables
			  updateStatus(thePLC);

			  if (thePLC->isSharedConnection())
			  {
                  std::ostringstream os;
                  os << "Shared connection with " << thePLC->getName() << " is closed.";
                  if (thePLC->theHeader_ != NULL) TRACE("warn") << os.str();
                      LOG(COMM) << os.str();
                  std::ostringstream os;
                  os << "Connection with " << thePLC->getName() <<
                      " (" << thePLC->theCluster_->getClassName() <<
                      "/v" << thePLC->theCluster_->getClassVersion() <<
                      ")" << " is closed.";
                  if (thePLC->theHeader_ != NULL) TRACE("warn") << os.str();
                      LOG(COMM) << os.str();
			  }
			}
			else
			{ LOG(COMM) << "Close connection with " << thePLC->getName() <<
						  " (" << thePLC->theCluster_->getClassName() <<
						  "/v" << thePLC->theCluster_->getClassVersion() <<
						  ")" << " has failed.";
			}
		}

	}


	//------------------------------------------------------------------------------------------------------------------------------------------------
	bool Connection::reOpen(PLC* thePLC)
	{
		if (ping((char *) thePLC->getName().c_str(), 0) == 0)
		{	doClose(thePLC, /*withLock =*/true);
			return doOpen(thePLC);
		}
		isAlive_ = false;
		return false;
	}

    bool Connection::isEnabled() { return isEnabled_; }
    bool Connection::isConnected() { return isConnected_; }


    //-------------------------------------------------------------------------------------------------------------------
    int Connection::readUnitCode(PLC* thePLC, UnitCodeType& dataStruct)
    {
        throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
        return -1;
    }


    int Connection::readUnitStatus(PLC* thePLC, UnitStatusType& dataStruct)
    {
        throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
        return -1;
    }


    int Connection::readCPUInfo(PLC* thePLC, CPUInfoType& dataStruct)
    {
        throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
        return -1;
    }


    int Connection::readCPInfo(PLC* thePLC, CPInfoType& dataStruct)
    {
        throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
        return -1;
    }

    bool Connection::isRunning(PLC* thePLC)
    {
        throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
        return -1;
    }

    int Connection::coldRestart(PLC* thePLC)
        {
            throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
            return -1;
        }

    //PERFORM COLD RESTART
       int Connection::plcStop(PLC* thePLC)
           {
               throw SilecsException(__FILE__, __LINE__, DIAG_PLC_REPORT_NOT_SUPPORTED, thePLC->getName());
               return -1;
           }

	//------------------------------------------------------------------------------------------------------------------------------------------------
	void Connection::updateStatus(PLC* thePLC)
	{
		//Connection status has changed (opened/closed)
		//Update the PLC diagnostic variables
		thePLC->updateStatus();

		//Reset reconnection mecanisme in case of connection succeed
		if (isConnected_)
		{ delayConn_ = UrgentConnection;   //initial reconnection delay
		  remainConn_ = numberConn_; 	   //initial number of attempt
		}
	}


	//------------------------------------------------------------------------------------------------------------------------------------------------
    void Connection::logError(PLC* thePLC, bool isReachable)
    {
        std::string errorMsg = isReachable ? "Connection with " + thePLC->getName() + ":" + thePLC->theCluster_->getClassName() +
                                              "/v" + thePLC->theCluster_->getClassVersion() + " has failed.\n"
                                           : "Controller " + thePLC->getName() + " does not respond to ping, might be OFF!\n";

		if (delayConn_ == LongTermConnection)
		{
		    if (remainConn_ > 0)
            {
                std::ostringstream os;
                os << errorMsg << "Periodic attempt to reconnect, delay " << delayConn_ << "s (tracing off).";
                if (thePLC->theHeader_ != NULL) TRACE("error") << os.str();
                LOG(COMM) << os.str();
                remainConn_ = 1; //Try to reconnect again and again (=1 means disable tracing).
            }
            /*else
              PLC does not respond anymore. It's probably stopped for a long time.
              Do not log error anymore (but still try to reconnect, with long-term delay).
            */
        else
        {
            std::ostringstream os;
            os << errorMsg << "Next attempt to reconnect in " << delayConn_ << "s if requested. Remains: " << remainConn_;
            if (thePLC->theHeader_ != NULL) TRACE("error") << os.str();
            LOG(COMM) << os.str();
        }
	}

	//------------------------------------------------------------------------------------------------------------------------------------------------
	bool Connection::isTimeToReconnect()
	{
		bool toBeReconnected = false;
		if (timeConn_ != NULL)
		{	//how many time from the last connect attempt
			double delay = timeConn_->getDelay(S_UNIT);
			if (delay >= double(delayConn_))
			{ 	timeConn_->getValue(S_UNIT); 	 //restart delay counting from now
				toBeReconnected =  true;

				if (remainConn_ > 0) --remainConn_;
				if (remainConn_ == 0)
				{	if (delayConn_ != LongTermConnection)
					{   if (delayConn_ == UrgentConnection)
							delayConn_ = ShortTermConnection;
						else //(delayConn_ == ShortTermConnection)
							delayConn_ = LongTermConnection;
						remainConn_ = numberConn_;
					}
				}
			}
		}
		else
		{
			//This is the first connection attempt, just start the Time counter.
			timeConn_ = new TsCounter(true); //using hardware clock
			timeConn_->getValue(S_UNIT); 	 //start delay counting from now
			toBeReconnected =  true;
		}
		return toBeReconnected;
	}
	  /* This macro is used to trap the unexpected broken pipe and
	     return an error instead of exit process.
	  */
	  static __sighandler_t sigpipeHandler = (__sighandler_t)-1;
	  #define _DISABLE_SIGPIPE sigpipeHandler = signal(SIGPIPE, SIG_IGN)
	  #define _ENABLE_SIGPIPE signal(SIGPIPE, sigpipeHandler)

	  int connect_nonb(int sockfd, struct sockaddr *saptr, socklen_t salen, int nsec)
	  {
	    int             flags, n, error;
	    socklen_t       len;
	    fd_set          rset, wset;
	    struct timeval  tval;

	    flags = fcntl(sockfd, F_GETFL, 0);
	    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

	    error = 0;
	    /*v1.1*/
	    _DISABLE_SIGPIPE;
	    n = connect(sockfd, (struct sockaddr *) saptr, salen);
	    _ENABLE_SIGPIPE;
	    if (n < 0){
	        if (errno != EINPROGRESS)
	            return(-1);
	    }

	    /* Do whatever we want while the connect is taking place. */

	    if (n == 0)
	        goto done;  /* connect completed immediately */

	    FD_ZERO(&rset);
	    FD_SET(sockfd, &rset);
	    wset = rset;
	    tval.tv_sec = nsec;
	    tval.tv_usec = 0;

	    if ( (n = select(sockfd+1, &rset, &wset, NULL,
	                     nsec ? &tval : NULL)) == 0) {
	        close(sockfd);      /* timeout */
	        errno = ETIMEDOUT;
	        return(-1);
	    }

	    if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
	        len = sizeof(error);
	        if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) {
	            return(-1);         /* Solaris pending error */
	        }
	    }

	  done:
	    fcntl(sockfd, F_SETFL, flags);  /* restore file status flags */

	    if (error) {
	        /*close(sockfd); just in case */
	        errno = error;
	        return(-1);
	    }

	    return(0);
	  }

	  /*----------------------------------------------------------*/
	  int rfcPing(char *ip, long ts)
	  {
	    int err, s, val = 1;
	    struct protoent *pent;
	    struct sockaddr_in rsock;

	    /* Socket create/connect */
	    memset ((char *)&rsock,0,sizeof(rsock));

	    if ((s=socket(AF_INET,SOCK_STREAM,0)) == -1)
	    {
	        LOG(ERROR) << "Can't create socket: " << strerror(errno);
	        return RFC_SYS_ERROR;
	    }

	    /* Set TCP_NODELAY option to force immediate TCP/IP acknowledgement */
	    if ((pent=getprotobyname("TCP")) == NULL)
	    {
	        LOG(ERROR) << "Can't configure socket: " << strerror(errno);
	        return RFC_SYS_ERROR;
	    }
	    if (setsockopt(s,pent->p_proto,TCP_NODELAY,&val,4) == -1)
	    {
	        LOG(ERROR) << "Can't configure socket: " << strerror(errno);
	        return RFC_SYS_ERROR;
	    }

	    rsock.sin_addr.s_addr=inet_addr(ip);
	    rsock.sin_family=AF_INET;
	    /*check any port to detect if the host is OFF*/
	    rsock.sin_port=htons(102);

	    err = 0;
	    if (connect_nonb(s,(struct sockaddr *)(&rsock),sizeof(rsock), ts) == -1)
	    {
	      /*if hostname is OFF, connect() fails on TIMEOUT*/
	        if ((errno == ETIMEDOUT) || (errno == EHOSTDOWN) || (errno == EHOSTUNREACH)) {
	        err = RFC_TIMEOUT_ERROR;
	      }
	    }

	        close(s);
	    return err;
	  }

	  /*..........................................................*/
	  int Connection::ping(char *hostName, char *plcIP)
	  {
	    struct in_addr addr;
	    struct hostent *hp;
	    char *ipstr = plcIP;
	    std::string errorMsg;
	    std::ostringstream os;

	    if ((hostName == NULL) && (plcIP == NULL))
	    {
	      os << errorMsg << "Bad parameter(s) value/format";
	      LOG(COMM) << os.str();
	      return -1;
	    }

	    // use hostName reference in priority else plcIP
	    if (hostName)
	    {
	      hp = gethostbyname(hostName);
	        if (hp)
	        {
	          addr.s_addr = *((unsigned long int *) hp->h_addr);
	          ipstr = inet_ntoa(addr);
	        }
	    }

	    if (ipstr == NULL)
	    {
	      os << errorMsg << "PLC hostname unknown";
	      LOG(COMM) << os.str();
	      return -1;
	    }

	    /*trying to connect PLC (1 second max)*/
	    return (rfcPing(ipstr, 1));
	  }

} // end namespace